The most expensive state a system can be in is halfway between two designs. Not the old way, not the new way, but both at once, indefinitely, with a thin layer of glue holding them together and one person who understands how.
Almost every long-lived system has at least one of these. The migration was announced, the first services moved, and then priorities changed. What remains is a permanent tax: two mental models, two sets of tooling, two failure modes, and an integration seam that exists solely because the work stopped.
Why they stall in the same place
The pattern is consistent. The first services to move are the easy ones, chosen deliberately to build momentum. They go well. The remaining ones are harder for reasons that were always true but did not get costed: an odd dependency, an owner who is not convinced, something nobody has touched in three years and is quietly afraid of.
Meanwhile the visible benefit has already been claimed. The demo happened, the win was reported, and the remaining eighty percent of the effort produces no new story to tell. The incentive to finish is much weaker than the incentive to start.
Plan the last one first
The most useful thing you can do before starting is to look at the hardest case and decide, honestly, whether it will ever move. If the answer is no, then you are not planning a migration. You are planning to run two systems permanently, and that is a legitimate choice but a different one, with different costs and a different design.
If you would not sign up to finish, do not start. A half-migration is usually worse than either endpoint.
Make progress visible and boring
A migration needs a number that only goes one direction, published where people see it. How many services are on the new thing, out of how many total. Not a percentage of effort or a status colour, a count.
It matters because it makes stalling visible. A migration that has not moved in two months is a decision somebody is making by not making it, and a public count turns that into a conversation.
Delete the old path deliberately
The migration is not finished when the last service moves. It is finished when the old path is gone: the code, the infrastructure, the pipeline, the documentation, and the alerting. Until then, both paths are load-bearing, because someone will find the old one and use it.
Removal should be a planned task with a date, not something that happens when someone gets around to it. It is also the most satisfying part of the work, and the only point at which you actually stop paying for the transition.
Strangle, do not rewrite
Where possible, the shape that works is to put the new thing beside the old one, route a slice of real traffic at it, keep the old path warm, and widen the slice only when the current one is boring. It takes longer to feel finished and it is dramatically less likely to end in a rollback with no plan.