Every legacy system eventually produces the same meeting. Someone lays out how much faster everything would be on a clean architecture, everyone agrees, and a rewrite gets scheduled for two quarters. Eighteen months later there are two systems, one of which earns money and one of which is nearly ready.

The alternative is not "live with it". It is to replace the system while it keeps running, one route at a time, with every step deployed to production and every step revertible. The patterns for this are old, well documented and boring, which is exactly what you want. This article is about how they fit together, and about the one section most write-ups leave out: when a rewrite genuinely is the right answer.

Why big-bang rewrites keep happening

They keep happening because they are genuinely appealing, and it is worth being fair about why. A rewrite offers a clean specification, an end date, and no compromises with decisions made by people who have left. Incremental replacement offers none of those. It offers a year of living with two systems at once.

Fowler names the flaw precisely: "Replacing a serious IT system takes a long time, and the users can't wait for new features. Replacements seem easy to specify, but often it's hard to figure out the details of existing behavior." Strangler Fig Application.

That second sentence is the whole problem. The specification for the replacement is the old system's behaviour, and nobody knows what that is. Not because the team is careless — because fifteen years of edge cases, workarounds and regulatory patches live in code that nobody has read end to end, and a good half of them are not bugs. They are the reason a particular customer stayed.

There is an organisational reason too, and ignoring it is why good technical arguments lose. A rewrite is easy to fund because it has a shape a budget understands: a scope, an end date, a number. Incremental replacement asks for a standing allocation with no completion date, which is a much harder thing to approve — even when it is cheaper. If you want the incremental plan to survive the meeting, give it the same artefacts: a sequence of dated milestones, each of which delivers something visible.

The risk curve nobody draws

Line chart of unreleased risk over time. A smooth curve rises steadily to a peak at a single cutover night, while a sawtooth line repeatedly rises a little and drops back to near zero.
The same total work, arranged so that being wrong is survivable.

Both approaches do roughly the same total amount of work. The difference is entirely in the shape of the risk, and specifically in how long you go between deciding something and finding out whether you were right.

Delivery research keeps pointing the same way. The 2025 DORA report found the largest group of teams sitting at an 8–16% change failure rate, with 39.5% above 16% — and only 16.2% able to deploy on demand. Read those next to each other and the case for small changes makes itself: if roughly one change in eight fails, you want changes to be small, frequent and individually revertible. Reporting on the 2025 benchmarks (secondary coverage, not the report itself).

A big-bang cutover is a single change with a failure rate you have never measured.

Finding the seams

Incremental replacement needs a place to cut, and legacy systems rarely offer one where you want it. A seam is any point where you can redirect a call without changing the caller. The work of the first month is finding them, and if there are none, making one.

  • At the edge. An HTTP route, a queue topic, a scheduled job. The easiest seams because a proxy can sit in front of them without the legacy code knowing.
  • At the data. Event interception — pick up the writes and mirror them outward. Fowler notes that if you are using the Strangler Fig you are using some form of this whether you named it or not.
  • Inside the code. Branch by abstraction: introduce an interface over the thing you want to replace, keep the interface unchanged, and swap the implementation behind it. Preferable to versioning the interface when there are many callers, because it narrows the surface of change.

Choose the first seam by risk, not by ambition. The right opening move is a route that is high in volume — so you learn quickly — and low in consequence, so that learning is cheap. Reporting endpoints and read-only views are usually where to start. Billing is not.

The strangler fig, concretely

The metaphor is a real plant: a vine that "germinate[s] in a nook of a tree… draws nutrients from the host tree until it reaches the ground to grow roots and the canopy to get sunlight." In software it means putting a facade in front of the old system, then moving what is behind it, one route at a time.

Three panels. First: a facade in front of a legacy system taking all traffic. Second: the facade routing one path to a new system and the rest to legacy. Third: the facade in front of the new system alone.
Three phases — but phase two is where you live for most of the project.

The objection is always the proxy: it is code that exists only to be deleted. Fowler answers it directly — "people often balk at the necessity of building transitional architecture… While this may appear to be a waste, the reduced risk and earlier value from the gradual approach outweigh its costs." Budget the transitional architecture explicitly. A plan that pretends it is free will quietly cut it, and cutting it means going back to a big bang.

For a worked example on this site, the day we turned the map off documents a national-scale map platform rebuilt this way over five months and three architectural generations — including the part where the flagship feature was deliberately switched off rather than limped along.

Expand and contract: migrating data without a maintenance window

Routing is the easy half. The schema underneath is where incremental migrations actually fail, because a database change is the one deploy people believe cannot be reverted. Parallel Change is the pattern that makes it revertible, and it has three named phases: expand, migrate, contract.

Five numbered steps along a track, grouped under expand, migrate and contract: add the new column, write to both while reading the old, backfill the history, read the new while still writing both, drop the old column.
Five deploys instead of one migration window.

Fowler is blunt about the failure mode: "if the contract phase is not executed you might end up in a worse state than you started." Step five is the one that gets deprioritised the week the new feature ships, and skipping it leaves two columns, both written to, neither authoritative, and a year later nobody remembers which one the reports read from. Parallel Change.

Put the contract step in the same ticket as the expand step, with its own acceptance criterion. It is the only reliable defence against a migration that is 80% done forever.

Shadow traffic: proving the new path before you trust it

You now have a seam and a schema that can move. What you do not have is confidence that the new implementation matches the old one, because the specification was never written down. Shadow traffic is how you recover it: send every real request to both paths, serve the user from the old one, throw the new one's answer away, and record the differences.

Flow diagram: a request forks to a legacy path that serves the user and a new path whose result is discarded; both results feed a diff comparator that logs every mismatch.
Production traffic, production data, zero production consequences.

The mismatch log is the specification you never had. Most entries will be trivial — key ordering, rounding, whitespace — and you tune those out. What remains is the interesting part: the cases where the old system does something surprising, and the moment where you have to decide whether it is a bug you are fixing or a behaviour a customer depends on. That decision is the actual work of a migration, and shadowing is what surfaces it before a customer does.

  • The new path must not write. Shadowing a mutation executes it twice. Start with reads, and for writes shadow into a scratch schema you can throw away.
  • Budget the duplicate load. You are running the system twice. If that is not affordable, shadow a sampled percentage — a few percent of real traffic beats a hundred percent of synthetic traffic.
  • Cut over on the data, not the calendar. The signal to switch is a mismatch rate that has been boring for a fortnight, not the end of the quarter.

Track parity as a number on a dashboard, next to latency and error rate: percentage of shadowed requests where the two paths agreed, broken down by endpoint. It turns a migration from a feeling into a metric, and it gives you something to show the people funding the transitional architecture — which, at month seven, you will need.

Cutover by feature flag, not by weekend

When the diff is quiet, the switch itself should be the least dramatic event of the project: a flag that moves a percentage of traffic from one implementation to the other, changed at 10am on a Tuesday by someone who can change it back in ten seconds.

  1. Move internal users first. They will report problems in a chat message rather than a support ticket.
  2. Then a percentage — 1, 5, 25, 50 — with a defined wait at each step and a named person watching the error rate.
  3. Keep the old path warm and reachable for as long as the rollback matters, which is longer than it feels like it should be.
  4. Delete it on a scheduled date, in a ticket that already exists. This is the contract phase again, and it is skipped for the same reasons.

One rule makes all of this work: the flag must be a runtime setting, not a deploy. If rolling back requires a release, your rollback takes as long as your pipeline, and during an incident that is the difference between a blip and an outage.

When a rewrite actually is the right call

Incremental replacement is the default, not a religion. It has real costs — transitional architecture, a long period of running two systems at once, and a discipline requirement that not every organisation can sustain. Sometimes a rewrite is simply correct.

Two columns of seven criteria each, headed strangle it and rewrite it, listing the conditions under which each approach is appropriate.
Count the rows on both sides before you decide.

The row that decides it most often is the second one on the right: whether the full specification fits on a page. If someone can write down what the system must do without reading the source, a rewrite is a bounded problem. If they cannot — and for anything that has been in production for a decade they cannot — then the rewrite is not a build project. It is a reverse-engineering project with a build attached, and the schedule was estimated for the build alone.

The honest reading of that table is that most real systems match several rows on both sides. When they do, the deciding question is not which column is longer. It is this: if the replacement turns out to be wrong, how long will it take to find out, and what will it cost to undo? Incremental replacement is not a better architecture. It is a shorter feedback loop, bought with transitional code.

Where to start next sprint

  • Put a facade in front of something. Even if it routes 100% of traffic straight through to the legacy system, you now have a place to stand. That is a one-sprint task and it unblocks everything else.
  • Pick a high-volume, low-consequence route. You want to learn fast and be wrong cheaply. Reports before payments, reads before writes.
  • Shadow it before you trust it. The mismatch log will teach you more about your own system in two weeks than any amount of archaeology in the code.
  • Write the deletion ticket now. Contract phases do not happen unless they were planned before anyone was tired. Give it a date and an owner in the same breath as the build.
  • Make the flag a runtime setting. A rollback that needs a deploy is not a rollback. This is the single cheapest piece of insurance in the whole plan.

None of these steps is impressive on its own. That is the point: at no stage is there an evening on which everything has to work.