The migration that never finished
Half-completed migrations are the most expensive state a system can be in, and the most common one.
Every mature codebase has at least one: the migration that got to 70% and stopped. The new system handles most traffic, the old one handles the awkward remainder, and both are maintained forever.
This is worse than either finishing or never starting, and it is the default outcome unless something prevents it.
why it costs more than both#
Two systems to maintain. Every change lands twice, or lands in one and silently diverges in the other.
Two sets of bugs, plus a third set caused by the interaction.
Nobody knows which is authoritative. New engineers ask; the answer is "it depends."
The benefit never arrives. The reason for the migration — delete the old thing, get the performance, simplify the model — is only realised at 100%. At 70% you have paid the full cost and collected none of the return.
It gets harder over time. The remaining 30% is the hard 30%: the weird integrations, the customer with the bespoke arrangement, the code nobody understands. And it gets harder as the people who understood the original migration leave.
why it happens#
Not laziness. The incentives genuinely point this way:
The easy 70% delivers most of the visible benefit. The graph goes up, the demo works, the announcement is made. The remaining 30% has no visible reward.
The hard cases are hard for a reason. They were skipped because someone did not know how to handle them, and that has not changed.
Priorities move. The migration was urgent in Q1. In Q3 there is a launch.
Nobody owns the finish. The person who drove it moved on, and completion was never anyone's explicit goal.
the mechanisms that actually work#
Name the deletion, not the migration. The project is not "migrate to the new pricing service." It is "delete the old pricing service." The deliverable is the deletion, and the migration is how you get there. This one rewording changes what people track and what counts as done.
Set a date, publicly, at the start. Not "by end of year" — a date, in the plan, with the deletion as the milestone. Dates without deletions slip silently; a date attached to "and then this code is gone" is checkable.
Make the old path visibly worse. Log a warning on every use. Add latency — genuinely, deliberately. Put a banner in the internal tool. The old path being comfortable is why nobody leaves it.
Count the stragglers on a dashboard. "Requests still on the legacy path" as a number that goes down, reviewed weekly. Anything not measured stalls at whatever level nobody notices.
Do the hard cases first. Backwards from the usual instinct and correct. The easy 70% will always be doable; the hard 30% is what determines whether the project is possible at all. Finding out in month one that a case cannot be migrated is a much better outcome than finding out in month nine.
Budget the finish before starting. If you cannot fund the last 30%, do not start. A migration you cannot complete is worse than the system you have.
the decision worth making explicitly#
Sometimes finishing genuinely is not worth it. The remaining cases are rare, the old path works, and the effort is better spent elsewhere.
That is a legitimate call — but it has to be made, written down, and the state made permanent rather than provisional:
The legacy importer stays for CSV uploads from the four enterprise customers using it. New integrations use the API. We are not migrating them; the importer is now a supported, frozen component with an owner, not a migration in progress. Reviewed 2026-09-21, revisit 2028.
That is a completely different thing from a stalled migration, even though the code looks identical. One is a decision with an owner. The other is a mess nobody has admitted to.
The cost of the second one is not the code. It is that every engineer who touches that area has to reconstruct which way things are supposed to be going, and nobody can tell them.
— Dom, September 21, 2026