Modernising a system that cannot stop running
The system everyone wants replaced is also the one the business runs on, which is why the rewrite gets proposed every eighteen months and abandoned every twenty-four. The alternative is unglamorous and works: replace it a piece at a time, with the old system running until each piece is proven.
Part ofCustom Software Development
A full rewrite is attractive because it promises a clean end state, and it fails for a reason that is structural rather than a matter of execution: for the entire duration, you are maintaining two systems, shipping business change into the old one, and reproducing that change in the new one. The finish line moves at the speed you approach it.
The four reasons rewrites stall
- The old system keeps changing. Business requirements do not pause, so the target is not a fixed specification.
- Nobody knows what it does. Behaviour accumulated over a decade is undocumented, and much of it is load-bearing for someone.
- Value arrives only at the end. Nothing ships for a year, so the project is a pure cost line at every budget review.
- The switch is one enormous event. All risk concentrates on a single date, which is why it slips repeatedly.
The strangler pattern, in practice
Put an interception layer in front of the old system, route one capability at a time to new code behind it, and let the old system shrink until what remains is small enough to retire or leave alone.
The property that makes this work is that every step is reversible in seconds. A rewrite's cutover can only be reversed by restoring a backup and losing everything since; a routed capability is reversed by changing a rule.
Choosing what to move first
Reporting and read-only views are usually the correct first move on every one of those criteria: loosely coupled, frequently requested, verifiable against the old output, and harmless if wrong for an hour.
The data is the hard part, not the code
Splitting behaviour is tractable. Splitting a database that everything reads from is where modernisation projects actually get stuck.
- One writer per table. Two systems writing the same rows produces conflicts nobody can reconcile after the fact
- Readers can be many, temporarily. The new system reading legacy tables is an acceptable interim state and a bad permanent one
- Replicate rather than share, where you must have both — a one-way copy with a clear owner beats two systems sharing a live table
- Move ownership with the capability, so a capability and its data cross together rather than leaving a remote dependency behind
- Expect to keep a synchronisation layer for the whole transition, and budget for it as a component rather than treating it as glue
The one-writer rule is worth being rigid about. Dual writes appear to work in testing and fail under concurrency in ways that produce silently divergent data — and by the time anyone notices, neither copy is trustworthy enough to declare correct.
Deciding what not to move
The unstated assumption in most modernisation plans is that everything must eventually be replaced. It is usually false, and dropping it is what makes the programme affordable.
Measuring the transition honestly
- Percentage of traffic served by new code — the only progress measure that cannot be argued with.
- Capabilities fully moved, including their data — a capability whose data still lives in the legacy database is not moved.
- Discrepancy count from the comparison runs, trending down before each switch.
- Change lead time for a typical request, which should improve as capabilities move, or the programme is not delivering its stated benefit.
- Legacy code paths retired, because code not deleted is code still maintained.
The final measure is the one that keeps a strangler honest. It is entirely possible to build new implementations of everything while the old paths remain live behind a flag nobody dares remove — and that produces two systems permanently, which is worse than either alone.
Questions
Why do full rewrites usually fail?
Because four things compound: the old system keeps changing while you rebuild, so the target moves; a decade of undocumented behaviour is load-bearing for someone and is not in the specification; no value ships until the end, so it is a pure cost line at every budget review; and all risk concentrates on one enormous switchover date, which is why it slips repeatedly.
What should be replaced first?
Usually reporting or read-only views. They score well on every criterion that matters: loosely coupled, frequently requested so the benefit is visible, verifiable against the old output, and harmless if wrong for an hour. Avoid starting with the payment path — prove the pattern somewhere failure is survivable.
How do I find behaviour that nobody documented?
Run the new implementation alongside the old, send both the same inputs, and log differences without acting on them. Weeks of that produce a list where each discrepancy is either a bug in the new code or a piece of undocumented behaviour you have just discovered. It is the cheapest specification available for a system nobody remembers writing, and it is unavailable to a rewrite.
Does everything have to be replaced eventually?
No, and assuming so is what makes modernisation unaffordable. A component unmodified in five years that has caused no incidents is finished software, not technical debt. The cost of legacy systems comes from the parts that must change frequently and resist it — usually a minority of the code. Wrap the quiet parts in an interface, monitor them, and spend the budget where change actually happens.