Skip to content
Software delivery6 min read

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

  1. The old system keeps changing. Business requirements do not pause, so the target is not a fixed specification.
  2. Nobody knows what it does. Behaviour accumulated over a decade is undocumented, and much of it is load-bearing for someone.
  3. Value arrives only at the end. Nothing ships for a year, so the project is a pure cost line at every budget review.
  4. 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.

text
Step 0  Everything goes to the old system.

        client ──▶ [ legacy ]

Step 1  Insert a routing layer that changes nothing yet. Deploy it
        and let it run for a week — proving the layer is transparent
        is a milestone on its own.

        client ──▶ [ router ] ──▶ [ legacy ]

Step 2  Route ONE capability to new code. Choose it by the criteria
        below, not by which is most interesting.

        client ──▶ [ router ] ──┬──▶ [ legacy ]
                                └──▶ [ new: reporting ]

Step 3  Repeat. Each move is independently deployable and
        independently reversible — the router flips back.

Step 4  What remains is either small enough to finish, or stable
        enough to leave. Both are acceptable endings; the second
        is more common and is not a failure.

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

CriterionPreferBecause
CouplingThe least coupled capabilityFewer seams to cut means a real chance of finishing it
Change rateSomething changing oftenThe new system's benefit shows immediately
Data ownershipSomething owning its own dataShared tables are the hardest part of any split
Blast radiusNot the payment path, firstProve the pattern where failure is survivable
ObservabilitySomething you can verifyYou must be able to tell whether the new one is correct
Business visibilitySomething someone cares aboutFunding continues when value is visible

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.

ComponentOften the right answer
Stable, rarely changed, worksLeave it. Wrap it in an interface and stop touching it
Batch job nobody has edited in five yearsLeave it, monitor it, document what it does
Feature used by three people once a quarterRetire it, or replace it with a manual process
Core workflow, changed weeklyMove it — this is where the cost actually is
Component blocking a platform upgradeMove it, because its cost is now everyone's

Measuring the transition honestly

  1. Percentage of traffic served by new code — the only progress measure that cannot be argued with.
  2. Capabilities fully moved, including their data — a capability whose data still lives in the legacy database is not moved.
  3. Discrepancy count from the comparison runs, trending down before each switch.
  4. Change lead time for a typical request, which should improve as capabilities move, or the programme is not delivering its stated benefit.
  5. 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.

Want this applied to your systems?

Most of what is described here begins as an assessment: a fixed-scope review producing a prioritised written report you can act on with any supplier.

Prefer email? hello@kornspire.com