Skip to content
Engineering18 Nov 2025 · 8 min read

Modernising a legacy system without stopping the business

How to replace a system that cannot be switched off: strangler-fig routing, CDC bridges, parallel running, feature flags, and when a rewrite is right.

DODaniel OkaforHead of Engineering
A screen full of source code

The systems most in need of replacement are usually the ones that cannot be switched off. The order-management application written in 2009, say, or the Access database that quietly runs invoicing, or the mainframe batch job that nobody wants to touch. They process real money every day, and "we'll cut over on a bank holiday weekend" is a plan that has ended a lot of careers.

We have replaced enough of these to have firm views. The short version is that you surround a legacy system and drain it. New code goes around the old and work moves across a piece at a time. Once nothing depends on the old system, you turn it off. Here is how that works in practice.

The strangler-fig pattern

The strangler-fig pattern (Martin Fowler named it after a plant that grows around a host tree until the host is no longer needed) is the default approach for anything with users depending on it. You put a routing layer in front of the old system, then move one capability at a time behind that layer to new code. Users notice nothing, because the front door has not moved.

The mechanics vary. For a web application it is often a reverse proxy or gateway that routes /invoices/* to the new service and everything else to the old one. A thick client or a batch system usually needs an API façade that the old code is taught to call instead. For a database-centred system it may start as nothing more than a read-only replica that new screens are built against.

What matters is that each increment is small enough to reverse. If a slice goes wrong, you flip the route back and try again next week. A big-bang cutover offers no such mercy. By the time you know it has failed, the old system's data is stale and the new one is full of half-processed transactions.

Data bridges and change data capture

The awkward part of running old and new side by side is data. Both systems need a consistent view of customers, orders or stock while responsibility is handed over piece by piece.

The tool for this is change data capture (CDC). Instead of polling, or dual-writing from application code, you tail the old database's transaction log (Debezium is the usual choice for SQL Server, Oracle, MySQL and Postgres) and stream every insert, update and delete to the new system as events. Latency is typically under a second, and the old application does not need to know it is happening.

A few hard-won rules:

  • Decide the system of record for each entity and make it explicit. During transition, customers might be owned by the old system and orders by the new. Never let both write the same table.
  • Bridge in one direction where you can. Bidirectional sync is possible, but it doubles the failure modes, so treat it as a temporary phase with an end date.
  • Reconcile continuously. A nightly job that compares row counts, checksums and a sample of records across both sides will find the drift that CDC lag, encoding oddities and triggers introduce. In our experience, reconciliation never comes back empty.
  • Keep the old schema's quirks out of the new model and translate at the boundary. If the legacy STATUS_CD column has 14 undocumented values, map them once, in one place, and log anything you have not seen before.

Parallel running and feature flags

For anything that produces numbers people are paid on (billing, commissions, payroll, regulatory returns), run old and new in parallel and compare the outputs before trusting the new one. Parallel running is the quickest way to find the rounding rule someone added by hand in 2014 and never wrote down, and doing it says nothing about how much you trust the new code.

As a worked example, take the replacement of a freight rating engine. The new service runs in shadow mode for six weeks. It calculates a price for every real quote, logs it, and is otherwise ignored. The comparison job flags, say, 3.2% of quotes as different in week one, mostly around fuel surcharge rounding and a "temporary" customer override table nobody had mentioned. By week five that is down to 0.04%, all of it explained. Only then does it start returning prices to users.

Feature flags make this operational. Route 5% of traffic, then a single customer segment, then a region, and watch error rates and support tickets at each step. Flags also give you the instant rollback that a deployment pipeline does not. Keep them short-lived and delete each one once its slice is fully cut over. A flag that outlives its migration becomes the next piece of undocumented behaviour.

Pick the order by risk

Most teams want to start with the easiest module, and it is usually the wrong choice. Start with the slice that teaches you the most about the integration and data problems while carrying little business risk if it goes wrong.

A rough ordering that has served us well:

  1. Read-only views and reporting. Low risk, and it forces you to build the CDC pipeline and understand the data.
  2. Peripheral write paths with clear boundaries, such as notes, attachments and reference data.
  3. Core workflows with parallel-run verification, one at a time, highest value first.
  4. The last 10%, meaning obscure screens, annual processes and the thing finance does at year end. Put these in the plan early, because they are the ones that surface in month eleven with a deadline attached.

Draw a dependency map of the old system before choosing. If the invoicing module reads the pricing table directly, you cannot move pricing without addressing invoicing. Those hidden couplings set the order far more than the code does.

Finding the undocumented behaviour

A legacy system is defined by what it does, which is rarely what the surviving documentation says. Someone will insist that "the system just rounds to two decimal places" and it will turn out to round differently for one product category because of a bug fix in 2011 that customers now rely on.

Treat the running system as the specification and instrument it. Turn on query logging. Capture the inputs and outputs of the module you are replacing for a few weeks and use that traffic to build characterisation tests, which record what the code does today whether or not anyone intended it. Then sit down with the business and decide which behaviours to preserve and which to fix. Write both lists down. The "fix" list becomes a change log you can point to when someone asks why a number moved.

Talk to the people who use the system as well as the people who own it. The person who has done month-end on it for nine years knows about the workaround spreadsheet, and is often the only one who does.

When a rewrite is justified

Everything above assumes incremental replacement is possible. Sometimes it is not, or the cost of doing it incrementally exceeds the risk it avoids. A rewrite is defensible when:

  • The system is small enough that a full replacement is a few months of work and can itself be parallel-run.
  • The runtime is dead. The hosting is out of support and there is nobody left to hire for it, so its security holes will never be patched.
  • The data model is so wrong that every strangler slice would need redoing once the model changes. If the core entity is misdefined, surrounding it only spreads the problem.
  • There is no usable seam. Some monoliths cannot be routed around because every screen touches every table. You may still parallel-run the whole thing, but you are building the replacement in one go.

Even then, run the rewrite as a shadow system with the same reconciliation as above. A cliff-edge cutover is still the wrong shape. And a rewrite that takes longer than about a year without delivering anything to users is a rewrite that will be cancelled, usually just before it would have finished.

Where to start

Spend two to four weeks mapping the old system before writing any new code. Entities, integrations, batch jobs, who uses what and how often, and where the money flows. That map tells you the first slice, the system of record for each entity and the size of the reconciliation problem. It is the core of how we run legacy modernisation engagements, and it is often the moment a client discovers the system is both smaller and stranger than they thought. If you have a system that cannot be turned off and cannot stay as it is, get in touch and we will help you find the first seam.

  • Legacy modernisation
  • Migration
  • Architecture
  • Risk