LegacyGENERALSCALE-SPECIFICCONTESTED

The Strangler Pattern

Put a routing layer in front of the old system, move one behaviour at a time behind it, expand until nothing is routed to the old system, then retire it. The old and new run together for a long time.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

The module is too large and too unprotected to change in place, and a rewrite is too risky. What is the third option?

The requirement

A 2011 order-management service handles pricing, inventory, fulfilment and returns in one 60,000-line codebase. The business wants four new fulfilment behaviours this year; each estimate comes back as a quarter.

The obvious build

Build the replacement alongside, get it to feature parity, then cut over in one release. The old system keeps running untouched in the meantime, so nothing is at risk until the switch.

Why it breaks

"Feature parity" is not knowable. The behaviours nobody documented are exactly the ones the replacement will miss, and they surface as customer escalations after the cutover (The Requirements Nobody States).

How it breaks as requirements change
  • "Feature parity" is not knowable. The behaviours nobody documented are exactly the ones the replacement will miss, and they surface as customer escalations after the cutover (The Requirements Nobody States).
  • The delivery gap is the real killer: for the six months of parity work, the four new behaviours have to be built twice — once in the old system to ship on time, once in the new one to keep parity — or not at all (The Risk in a Rewrite).
  • The single cutover concentrates all the risk into one evening, which is precisely the shape of risk that cannot be tested in advance.
  • And the fallback is a full rollback of six months of divergence, which after the first hour of live traffic is usually no longer possible because data has been written in the new shape.
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • The service processes orders continuously; there is no window in which it can be switched off.
  • The four new behaviours have delivery dates, so any approach that delivers nothing for six months is rejected regardless of its merits.
  • The old system owns its own database schema, and several reporting tools read that schema directly (State Ownership).
  • The team is six people, which is not enough to run a parallel rewrite and keep the old system alive.
Invariants
  • At every moment, exactly one system is authoritative for any given behaviour. Two systems both believing they own returns is the failure this pattern is most likely to produce.
  • Callers must not need to know which system handled their request; the routing layer is the only thing that knows (Information Hiding).
  • The old system stays deployable and correct until the moment it is deleted. A strangler that breaks the old path has become an outage, not a migration.

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • The routing layer owns exactly one decision: for this request, which implementation is authoritative. It owns no business logic, and the pressure to give it some is constant (API Gateway in Architecture, and the same warning applies).
  • Each migrated slice owns its behaviour completely, including its data, or the slice is not actually migrated (Consistency Boundaries).
  • Someone owns the retirement of the old system as a deliverable with a date, because a strangler that never finishes is the most expensive outcome available.
Boundaries
  • The routing boundary should sit where callers already are — an HTTP path, a queue topic, a facade class — so that introducing it changes nothing for anybody on day one.
  • Slices are chosen by behaviour, not by layer. "Returns" is a slice; "the persistence layer" is not, because migrating it moves no behaviour and delivers nothing (Vertical Slices).
  • The hardest boundary is data. If the old and new systems share tables, the slice is not independent and you have bought coordination rather than isolation (Data Migration).

The shape: intercept, divert, expand, retire

The pattern is named after a fig that grows around a host tree and eventually replaces it. The useful part of the metaphor is that at no point is the tree absent — the new structure is load-bearing before the old one is removed.

Structurally there are only three moving parts: an interception point that everything already goes through, a rule about which implementation is authoritative per behaviour, and a plan for who deletes the old code.

  • The routing layer knows which system is authoritative per behaviour. Nothing else in the system knows, and nothing else should.
  • The anti-corruption layer exists so the new system does not inherit the old model as the price of reading old data (Anti-Corruption Layer).
  • The six reporting tools reading the legacy schema directly are the dependency that will decide when the old database can actually be deleted — usually much later than the old code.
Mid-migration: three slices moved, four still routed to the old system
migrated behaviourseverything elseownsownsreads what is not yet migratedCallers (unchanged)Routing layer — per-behaviour authorityNew: pricing, returns, quotesAnti-corruption layerNew schemaOld OMS: inventory, fulfilment, reporting, batchLegacy schema (+ 6 reporting tools)
UserLLMAgentToolDataDecisionHumanGuardrail

One slice, start to finish

The unit of progress is a slice: one behaviour, its data, its tests and its traffic. A slice that migrates behaviour but leaves data behind has not finished, and will pull the team back across the boundary on the next requirement.

Shadow comparison on live traffic is the step that separates strangler migrations that surprise nobody from the ones that produce an incident. It is also the step that reliably gets cut for time.

Migrating the "returns" slice
  1. 1
    Characterize the old slice

    Pin what returns processing currently does, including the odd cases, so the reimplementation has a specification.

    fails by Reimplementing from the ticket description and rediscovering four years of undocumented rules in production (Characterization Tests).

  2. 2
    Implement behind the router

    Build the slice in the new system; route zero traffic to it.

    fails by Building it as a copy of the old structure, which migrates the code and not the design.

  3. 3
    Shadow

    Run both on live traffic, serve the old result, log every divergence.

    fails by Comparing on synthetic traffic only, which never contains the inputs that matter.

  4. 4
    Move the data

    Give the slice its own storage; backfill; dual-write only for as long as the cutover needs (Expand and Contract).

    fails by Leaving the slice on the old schema, so it is migrated in name and coupled in fact.

  5. 5
    Shift traffic

    Route a percentage, watch, increase. Keep the route-back available and tested.

    fails by A single cutover, which reintroduces exactly the risk shape the pattern exists to avoid.

  6. 6
    Retire

    Delete the old returns code and its tests. Announce it. Remove the routing entry.

    fails by Leaving it in place "just in case", which is how a codebase ends up with two implementations of everything and no way to tell which is live.

Every slice ends with a deletion. A strangler measured in slices-implemented rather than slices-retired is measuring the wrong number.

Against the alternatives, with the numbers the matrix cannot hold

The honest comparison is three-way, and the third option — refactor the old system in place with the legacy change loop — is the one that gets skipped even though it is frequently correct for a single painful module (The Legacy Change Loop).

Scores here are relative judgements about a specific situation: a 60,000-line service, six engineers, continuous traffic and four dated deliverables. Change any of those and the ordering changes.

Three ways out of a 60,000-line unprotected service
OptionSimplicityFlexibilityPerformanceTestabilityOperationalMigration costNote
Refactor in place (legacy change loop)No new components, no dual operation, no migration risk. Bounded by the fact that it improves the paths you touch and nothing else — and it cannot escape the old system's data model.
StranglerDelivers new behaviours in the new system while the old one keeps running. Costs a routing layer, dual operation and a long tail. Fails by stalling rather than by breaking.
Rewrite with cutoverCleanest destination and the shortest path to it *if it works*. All risk lands on one day, feature delivery stops for the duration, and hidden requirements surface after cutover (The Risk in a Rewrite).

caveat These numbers cannot express the two things that actually decide it: whether your organisation will fund the last 20% of a strangler, and how much of the old system's behaviour is genuinely unknown. If the unknown fraction is large, the rewrite's migration score is worse than 1 and no amount of planning fixes it; if your organisation reliably finishes what it starts, the strangler's operational cost is a rounding error. Neither is a property of the code, which is why this is not an engineering decision made by engineers alone (The Trade-off Matrix).

How to build it

Most important first.

  • Insert the routing layer first, with everything still routed to the old system. This step is behaviour-neutral, ships on day one, and is the only structural prerequisite.
  • Choose the first slice for *learning*, not for value: something small, well-bounded and low-risk, so that the first migration teaches you what your routing, data and verification story actually is.
  • Migrate one slice: implement the behaviour in the new system, route a fraction of traffic to it, compare outputs against the old system on live traffic, then route the rest (Incremental Migration).
  • Move the data with the slice. A slice whose data still lives in the old schema will pull you back through the boundary the first time a requirement changes (Expand and Contract).
  • Expand slice by slice, always keeping the old path working, and always with the ability to route back.
  • Retire deliberately: when a slice has no traffic, delete the old code, and say so publicly. Undeleted dead code is the tax this pattern accrues if nobody is accountable for the last step.
  • Build the new behaviours the business asked for *in the new system, as slices*. That is what makes the migration fundable — it delivers features rather than competing with them.

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • During the strangler, every change is more expensive, not less: you have two systems, a routing layer, and a rule about which one a change belongs in. Anyone selling this as an immediate saving is wrong.
  • A change to an already-migrated slice costs what a change in a well-structured new system costs — a day rather than a quarter. That is where the return comes from, and it arrives per slice rather than at the end.
  • A change spanning a migrated and an unmigrated slice is the worst case in the whole scheme: it touches both systems and the routing layer, and it is more expensive than it was before the migration started. Sequence slices to make this rare (Change Amplification).
  • After retirement, change cost is that of the new system, and the routing layer can often be removed — one more deliberate step that nobody budgets for.
What the recommended approach costs
  • You operate two systems for the duration, with two sets of deployments, alerts, on-call knowledge and dependency upgrades. For a six-person team that is a substantial and continuous cost (Bus Factor).
  • The routing layer is real latency, a real failure point and a real place for bugs to live.
  • It is slower to a finished new system than a rewrite would be if the rewrite went well. The pattern is not optimising for speed; it is optimising for never having a day where everything is at risk at once.
  • It biases toward preserving the old system's boundaries, since slices are usually carved along lines the old system already drew — which can quietly reproduce the structure you were trying to escape.

What can go wrong

Failure modes
  • The strangler stalls. Two slices are migrated, the funding moves elsewhere, and the organisation now operates two systems, two deployment pipelines and a routing layer, indefinitely. This is the characteristic failure and it is more common than completion.
  • The routing layer accumulates logic — a special case here, a data fix-up there — and becomes a third system that is harder to change than either of the other two.
  • A slice is migrated but its data is not, so both systems write to the same table and the invariant that one system is authoritative quietly stops holding (The Dual Write Problem in Backend).
  • Comparison against the old system is done on synthetic traffic only, so the divergences that live traffic would have shown up are discovered after the old path is deleted.
  • The mitigation fails on its own terms: the ability to route back becomes theoretical once the new system has written data the old one cannot read, and nobody tests the route-back path after month two.
Dependencies, and their direction
  • Every caller now depends on the routing layer, which becomes a single point of failure and a coordination point. That is the price of the indirection and it is not small (Fan-in and Fan-out).
  • The new system depends on the old one for anything not yet migrated, often through an anti-corruption layer that translates the old model rather than adopting it (Anti-Corruption Layer).
  • Reporting tools depend on the old schema directly, which is a dependency nobody declared and which will discover itself at the least convenient moment.
Misreads
  • "Strangler means microservices." The pattern is about routing and incremental replacement; the new slices can be modules inside the same deployable, and for a six-person team they usually should be (The Modular Monolith).
  • "The routing layer is just a proxy, so it is free." It is a new component on the critical path of every request, owned by someone, deployed by someone, and paged on by someone.
  • "We can decide the slices up front." The first migrated slice reliably teaches you that your estimate of a slice's cost was wrong. Plan the first one, and re-plan after it.
  • "Once we are 80% migrated we are basically done." The last 20% is where the behaviours nobody understood live, which is why they were left for last. Budget for it explicitly rather than discovering it (The Requirements Nobody States).
  • "This avoids the rewrite risk entirely." It converts one large risk into many small ones and adds a coordination cost. That is usually a better bet, but it is a different bet, not the absence of one (The Risk in a Rewrite).

Testing it, and how it ages

What to test, and at which boundary
  • Contract tests at the routing boundary that both implementations must satisfy: this is the only artefact that makes "the new slice behaves like the old one" checkable rather than asserted (Contract Tests).
  • Shadow comparison on live traffic — run both, serve the old, log the differences. Nearly every strangler that succeeded had this, and nearly every one that produced a surprise did not (Incremental Migration).
  • Test the route-back path on a schedule, not once. Its value decays silently and you will want it during an incident.
  • Characterize the old slice before reimplementing it, because the reimplementation needs a specification and the old code is the only one available (Characterization Tests).
How this design ages
  • The routing layer is temporary in principle and permanent in practice unless someone owns removing it. Decide up front which it will be, and write it down (Decision Records).
  • Slice order should be revisited as you learn: the first slice teaches you the real cost of a slice, and that number usually invalidates the original plan.
  • A strangler that runs for more than about eighteen months tends to acquire a second generation of engineers who have never seen the old system work, and the retirement step becomes politically harder, not easier.

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • GENERALRouting behaviour incrementally to a replacement works wherever there is an interception point — an HTTP path, a message topic, a facade, a database view — so the shape holds from mainframes to serverless.
  • SCALE-SPECIFICAt the scale of one module inside one deployable, the "routing layer" is a facade class and the whole pattern is a week of work with no operational cost. At the scale of a service with its own database and downstream consumers, it is a multi-quarter programme with two on-call rotations. The name is the same and the decision is not remotely the same.
  • CONTESTEDThe strongest opposing view: strangler migrations very frequently stall at 40%, leaving permanent dual operation, and organisations that would have completed a rewrite in nine months instead run two systems for four years. Practitioners who have lived through a stalled strangler argue that the pattern's incrementalism removes the forcing function — nothing is ever urgent, so nothing finishes — whereas a rewrite with a cutover date concentrates attention. The counter is that a stalled strangler still leaves a working system while a stalled rewrite leaves nothing, but the failure rate objection is real and should decide slice sequencing: migrate the slices that are painful *now*, so each step is independently worth having (The Risk in a Rewrite).

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • System Design — routing a fraction of live traffic to a second implementation and comparing results is a traffic-shaping and capacity problem before it is a design one.