Legacy modernization succeeds when it respects the system that kept the business running.
The polite path is incremental: isolate one workflow, create a reliable integration boundary, prove the replacement in production, then repeat.
The impolite path is the rewrite. It is faster on paper, more satisfying to plan, and it is where modernization budgets go to die.
Why The Rewrite Is Tempting
The reasoning is always sound in isolation.
The old system is genuinely bad. The technology is unsupported, the original authors left years ago, changes take weeks, and every release carries risk. A clean implementation would be faster to change, cheaper to run, and easier to hire for. All true.
The flaw is not in the assessment. It is in the estimate — and the estimate fails for a structural reason.
The old system’s true specification is not written down anywhere. It is encoded in the behaviour of the running code, including every undocumented edge case fourteen years of business reality forced into it.
That VAT calculation with the odd rounding rule is not a bug. It matches what the finance team has been reporting since 2013. The batch job that runs at 02:40 rather than 02:00 avoids a lock contention issue with a system that was decommissioned but whose replacement inherited the schedule. Nobody knows this. It just works, and it will keep working until you rewrite it.
A rewrite promises to reproduce all of that from a specification that does not exist. The gap between what was documented and what the system actually does is where the schedule goes.
What Actually Happens
The failure mode is predictable enough to be a template.
The new system is scoped against the documented behaviour. Six months in, it handles the main paths well. Then the edge cases start arriving — usually from users, in production, after a partial cutover, framed as urgent.
Meanwhile the old system cannot be frozen, because the business keeps needing changes. So now two systems are being maintained, the new one is chasing a moving target, and the team is split. The date slips. Confidence erodes. At some point the programme is either cancelled with the old system still running, or forced live before it is ready.
Both outcomes are expensive. The second is worse.
flowchart TD
A[Rewrite begins] --> B[Scoped from documented behaviour]
B --> C[Main paths working]
C --> D[Undocumented edge cases surface]
D --> E[Old system still changing]
E --> F[Two systems maintained in parallel]
F --> G{Budget holds?}
G -->|No| H[Cancelled, legacy remains]
G -->|Yes| I[Forced cutover before ready]
The Strangler Fig
The alternative takes its name from a plant that grows around a host tree, gradually taking over its structure until the original decays away and the new form stands on its own.
Applied to software: you put a boundary in front of the old system, route traffic through it, and replace functionality behind that boundary one piece at a time. At every point there is exactly one production system — the composite — and it works.
The properties that matter:
- Every step ships. No six-month gap between starting and delivering value.
- Every step is revertible. If a replaced component misbehaves, route back to the original.
- The old system stays authoritative until each specific piece is proven.
- Risk arrives in small pieces instead of concentrated at a cutover date.
It is slower in aggregate than a rewrite that goes perfectly. Rewrites do not go perfectly.
Picking The First Seam
The first slice sets the tone politically as much as technically. Choose it carefully.
Good candidates share these traits:
- Clear boundaries. The functionality has identifiable inputs and outputs, and limited entanglement with everything else.
- Real but bounded pain. Something people complain about, so the win is visible — but not the most business-critical path in the system.
- Low write coupling. Read-heavy paths are much easier to run in parallel and compare than paths that mutate shared state.
- Observable correctness. You can tell whether the new implementation agrees with the old one.
Deliberately avoid, for the first slice: anything touching money movement, anything with a regulatory reporting obligation, and anything whose failure is invisible until month-end.
The first slice is not about the largest win. It is about establishing that the approach works, building the boundary infrastructure, and earning the credibility to do the harder ones.
The Boundary Is The Real Work
Most of the engineering value sits in the interception layer, not in the replacement components.
You need a point where traffic can be inspected and routed — a proxy, a facade service, a feature-flagged branch inside the existing application. What matters is that it can:
- Route a request to old or new based on rules you control at runtime.
- Change that routing without a deploy.
- Send the same request to both implementations and compare.
- Record which path served each request, for debugging and audit.
Getting this right once pays for every subsequent slice. Getting it wrong means each replacement is a bespoke cutover with its own risk profile.
Route by the narrowest dimension you can — a single customer, an internal user group, a percentage of traffic — so the first real exposure is small and specific.
Data Is Where This Gets Hard
Everything above is manageable. Data is the part that requires actual judgment.
The old and new components usually need the same data, and you have three options, in descending order of preference:
New reads from the old store. The legacy database stays authoritative; the new component reads it directly or through an API. No synchronisation, no divergence, no consistency problem. Ugly, and correct. Most slices should do this for as long as possible.
New owns a subset, old reads through an interface. Ownership genuinely moves for a bounded set of entities. Requires the old system to be modified to stop writing them, which is only viable where that code is well understood.
Bidirectional synchronisation. Both systems write, changes replicate. This is where modernisation programmes go to die. You have signed up for conflict resolution, ordering guarantees, and a permanent class of bug that only appears under concurrency.
If a slice appears to require option three, the seam is in the wrong place. Move it.
Proving It Before Trusting It
The step teams skip, and the reason they end up debugging in production.
Shadow running. Send production traffic to both implementations. The old one serves the response. The new one runs and its output is recorded, not returned. Compare the two. This is free correctness testing against real traffic including all the inputs nobody would have thought to write a test for.
Let it run longer than feels necessary. Weeks, not days — long enough to catch the month-end path, the quarterly batch, the annual renewal.
Then route real traffic, narrowly. One internal team. Then one friendly customer. Then a percentage. At each stage, the mismatch rate has to be zero or explained.
Explained matters. Some differences are the new system being correct where the old one was wrong. Those get documented and signed off by someone from the business — never quietly accepted by an engineer who assumes the new behaviour is better.
flowchart LR
A[Request] --> B[Boundary]
B --> C[Legacy: serves response]
B -.shadow.-> D[New: recorded only]
C --> E[Response to user]
C --> F[Comparator]
D --> F
F --> G{Mismatch?}
G -->|Yes| H[Investigate before<br/>any traffic shift]
G -->|No| I[Confidence to route]
Deleting The Old Code
The step that gets deferred forever, and the one that determines whether any of this was worth it.
Once a slice is fully migrated and stable, the old implementation must be removed. Not disabled behind a flag. Not left in place “just in case.” Deleted.
Code that still exists still gets maintained, still shows up in security scans, still confuses the next engineer, and still tempts someone into routing back to it during an incident rather than fixing the new path.
Set the deletion date when the slice starts. Make it a deliverable of the slice, not follow-up work. A modernisation programme that never deletes anything has not modernised — it has added.
When Not To Strangle
Being honest about the exceptions.
Genuinely small systems. If the whole thing is a few thousand lines and one person understands it, a rewrite may be a two-week job. The strangler overhead is not worth it.
The platform is going away. Hardware being decommissioned, a runtime losing support with a hard date, a vendor exiting. Sometimes the timeline is externally imposed and the incremental path does not fit inside it. Then you rewrite, and you plan for it honestly rather than pretending the risk is lower than it is.
The business process is changing anyway. If the organisation is genuinely abandoning the old workflow rather than reimplementing it, preserving legacy behaviour is not a virtue. You are building something new that happens to replace something old.
The test: are you trying to preserve existing behaviour, or replace it? Preservation favours strangling. Genuine replacement may not.
What It Looks Like With Agents
Agentic delivery changes the economics here in a specific way.
Shadow comparators, boundary routing, characterisation tests that pin down what the old system actually does — this is exactly the kind of high-volume, well-specified, tedious work that agents absorb well. Historically it was the reason teams skipped these steps: the scaffolding cost more than the slice.
The judgment does not move. Where the seam goes, which slice is first, whether a mismatch is a bug or a correction, when data ownership can safely transfer — those remain with the engineer, because they are decisions about consequences in a system nobody fully documented.
What changes is that the disciplined path stops being the expensive one.
The Bottom Line
The rewrite fails because the specification you need is not in any document. It is in the running code, including every accommodation the business made over a decade.
The polite path respects that. Put a boundary in front. Replace one bounded slice. Shadow it against real traffic until it agrees. Route narrowly, then widely. Delete the old code. Repeat.
It is less satisfying to plan and considerably more likely to finish. In legacy modernisation, finishing is the only outcome worth optimising for.