The system everyone complains about is usually the one running the business. It processes the orders, holds fifteen years of history, and encodes rules nobody wrote down after the person who knew them left. That is why the big-bang rewrite keeps being proposed and keeps failing: the new system must be complete before it is useful, and by then the old one has moved on.
The alternative is to replace it in pieces while it keeps running: harder in the short term, far more likely to finish.
Strangler fig, in practice
The pattern is named after a plant that grows around a host tree until the host rots away and the new structure stands alone. In software: put a routing layer in front of the legacy system, send all traffic through it, then move capabilities to the new implementation one routing rule at a time. Nothing else changes for the caller.
The first move is not writing new code. It is inserting the seam — usually a reverse proxy or API gateway in front of the existing application, routing everything straight through, stable before any migration begins. Boring, invisible to users, and the thing that makes everything afterwards reversible.
# Phase 1: everything to legacy
/* -> legacy
# Phase 3: one capability moved, one shadowed
/api/invoices/* -> billing-service
/api/customers/* -> legacy (mirrored to customer-service, responses compared)
/* -> legacy
Two things make the seam earn its keep. Shadow traffic: send a copy of live requests to the new implementation, discard its responses, compare them against the legacy output. That is production-grade validation at zero production risk, and it surfaces undocumented behaviour. And per-capability flags rather than one global switch, so any cutover’s blast radius is a single route.
Slice by domain, not by layer
The most common way to get this wrong is to migrate horizontally: first the database, then the service layer, then the UI. It maps neatly to how the codebase is organised, and it fails because no horizontal slice delivers value on its own. You cannot release “the new database” to a user; you accumulate months of work with nothing shipped.
Vertical slices by business domain — invoicing, scheduling, notifications — cross every layer and can go live independently. Choose the first by these criteria, roughly in order:
- Loosely coupled. Few reads and writes shared with the rest of the system. Notifications are often a good first candidate; the core transactional entity never is.
- Understood. Somebody can still explain the rules. Starting with the module nobody comprehends means learning the requirements and the migration mechanics at once.
- Painful enough to matter. It should relieve something people actually complain about, so the programme shows value before the budget conversation.
- Reversible. If it goes wrong, you can route back without a data repair job.
Where the boundary is unclear, the legacy database tells you. Tables written by one part of the application and only read elsewhere mark a natural edge. Tables written from everywhere are the shared core; they come last.
Moving the data: dual writes and reconciliation
Data is where these programmes get stuck. The sequence that works has four stages; the pain comes from skipping one.
- Backfill. Copy the historical data across idempotently, so you can rerun it. This is where you find the duplicates, the impossible dates, the free-text field holding four formats. Do not clean silently — log every transformation, because someone will ask why a number changed.
- Dual write. New writes go to both stores. The old one remains authoritative and its write must succeed; a failure against the new store is logged and retried, never blocking the business. Write asynchronously via change data capture or an outbox table, sidestepping distributed transactions.
- Reconcile continuously. A scheduled job compares the stores and reports divergence — record counts, checksums over key fields, spot comparisons. Dual writes drift; the only question is whether you find out from a report or from a customer.
- Flip authority. Once divergence has been zero for long enough to cover your slowest business cycle — month-end, quarterly billing, whatever the rare path is — make the new store authoritative and keep writing back to the old one. That reverse dual write is what makes rollback possible.
Keep the legacy identifiers rather than remapping them; the mapping table becomes a permanent source of bugs. And publish the reconciliation report where non-engineers can see it — when finance can check the numbers, the cutover conversation changes character.
Living with both systems
There will be a period — quarters, not weeks — when both systems are alive and both matter. Plan for it rather than treating it as an embarrassing interlude.
The legacy system stays supported: security patches, incident response, and some new features. A feature freeze sounds disciplined and rarely survives a regulatory change or a large customer’s requirement. Decide the rule in advance — changes to an already-migrated domain go only into the new system, everything else into the old one — and hold the line.
Set an end date for coexistence and treat slipping it as a serious event. Migrations that lose momentum do not pause; they settle. The organisation adapts to two systems, the glue becomes load-bearing, and you maintain both permanently — the worst available outcome, and a common one.
Rollback criteria, decided before you need them
Write the rollback conditions down before cutover, with numbers, and agree who can call it. At three in the morning, with a director on the phone, nobody negotiates a threshold well.
- Specific triggers: error rate above an agreed level for an agreed duration, latency beyond a stated percentile, any data integrity discrepancy, any incorrect financial figure.
- A named person per cutover who can decide alone, without convening a meeting.
- A rehearsed rollback procedure, not merely a written one. If reverting means flipping a route, prove it in staging under load. If it means restoring data, time it.
- An honest point of no return: the moment after which rolling back costs more than fixing forward, typically once the new store has accepted writes you cannot replay backwards.
- An agreement that invoking rollback is a good outcome. Teams that treat it as defeat stop invoking it, then ride out incidents they could have ended in five minutes.
Migrate, rewrite, or leave alone
Not every legacy system deserves the effort. Three questions separate them.
Is the business logic valuable or merely old? A system encoding two decades of pricing rules and hard-won exceptions holds real knowledge, and a from-scratch rewrite means rediscovering it through production incidents. Incremental migration preserves it. A system that is mostly forms over a database, whose rules fit on two pages, is a rewrite: the migration machinery would cost more than the software.
Is it changing? A system nobody has modified in three years, cheap to run and rarely broken, is not a problem simply because its technology is unfashionable. Isolate it behind an interface, keep it patched, and spend the money where change is being demanded. Legacy means “resistant to the change we need”, not “written in something we no longer enjoy”.
What does failure cost? Where an outage stops revenue or breaches an obligation, incremental migration with shadow traffic and reconciliation is the only responsible route. Where the worst case is an afternoon of manual work, be blunter.
The part that is not technical
The people who use the old system daily are faster on it than they will be on anything new for months. Their resistance is not irrational: the migration trades an immediate loss of competence for a promise of future benefit. Someone who spent years learning a green-screen terminal is being asked to become a beginner again.
What helps: involve the heaviest users in defining each slice, because they know the exceptions the specification omits, and a system they helped shape is one they defend. Migrate the workflows they hate before the ones they have mastered. Train on the real system with real data before cutover, not with slides afterwards. And be candid that the first weeks will be slower — a promised improvement that does not materialise costs the credibility you need for the next slice.
Where to start
Not with a technology stack. Draw the domain boundaries as they exist in the data, then put a routing layer in front of the current system and prove it under production traffic. If you cannot get that far — nothing can safely sit in front, or nobody can describe how the data flows — that is the finding. Fix it before committing a budget to the rest.