Skip to content

Strangler Fig and Branch by Abstraction

Replacing a system without a big-bang rewrite — intercept and divert, or build behind a seam, and how to tell which one you need.

Advanced19 minUpdated 8 August 2026

Two patterns for the same problem: replacing something substantial without stopping to rewrite it. Both exist because the big-bang rewrite fails reliably — it delivers no value until the end, competes with feature work it cannot win against, and its scope is discovered rather than known.

They apply at different levels, and picking the wrong one is a common source of stalled migrations.

Strangler fig: intercept at the boundary

Put a facade in front of the old system. Route each request either to the old system or to a new implementation. Move routes across one at a time. When nothing routes to the old system, delete it.

Conceptual diagram/orders — migrated/invoices — migratedeverything elseClientsFacade / routerNew serviceLegacy monolithNew storeLegacy store
Diagram, as text
  • Clients leads to Facade / router
  • Facade / router leads to New service (/orders — migrated)
  • Facade / router leads to New service (/invoices — migrated)
  • Facade / router leads to Legacy monolith (everything else)
  • New service leads to New store
  • Legacy monolith leads to Legacy store

It works because every step ships. Each migrated route is live, in production, delivering value, and reversible by flipping a routing rule. There is no six-month branch and no moment when everything changes at once.

The requirement is an interception point — a load balancer, a gateway, a proxy, a facade. Without somewhere to make the routing decision, the pattern does not apply, and creating that interception point is often the first and largest piece of work.

The hard part is not the routing, it is the data. Two systems reading and writing overlapping data is where migrations fail:

  • Old writes, new reads. Workable if the new system can read the old store, or if changes are replicated to it.
  • Both write. The dangerous case. It needs a decided source of truth per entity, synchronisation in at least one direction, and an accepted staleness window. Two-way synchronisation is where these projects go wrong, and it should be avoided by splitting on entity boundaries rather than on endpoints.
  • Migrate by entity where possible, not by endpoint. All the endpoints that touch orders should move together, so orders have one owner throughout.

Branch by abstraction: build behind a seam

When there is no boundary to intercept — a library, a persistence layer, an internal module used from a hundred places — you cannot route around it. So you create a seam instead.

  1. Introduce an abstraction over the thing being replaced, and route all existing callers through it. This step alone is often the largest, and it changes no behaviour.
  2. Add a second implementation behind the abstraction, incomplete at first.
  3. Switch callers across gradually, controlled by a feature flag, per caller or per percentage of traffic.
  4. Delete the old implementation and, usually, the abstraction with it.

Everything stays on the main branch and everything stays releasable, which is the property that distinguishes it from a long-lived migration branch that diverges until it cannot be merged.

The step that is skipped and should not be: run both and compare. With the abstraction in place, you can call both implementations, return the old result, and log any disagreement. That gives real production traffic as a test suite before anything depends on the new implementation — and it is the only realistic way to gain confidence in a replacement for behaviour nobody fully understands.

Choosing between them

Strangler figBranch by abstraction
Levelsystem or servicemodule, library, layer
Needsan interception pointa seam you create
Unit of migrationa route, endpoint or capabilitya caller or a percentage of traffic
Rollbackchange a routing ruleflip a flag
Main riskdata ownership across two systemsthe abstraction leaks the old design

The risk specific to branch by abstraction deserves naming: an abstraction extracted from the old implementation will encode the old implementation's assumptions, and the new one then has to satisfy an interface shaped by what it is replacing. This is the same failure described for ports and for Bridge seams — design the abstraction from what callers need, not from what the old code does.

Making it finish

Both patterns fail the same way: they stall. Three things reliably prevent it:

  • A visible number. Routes remaining, or callers remaining. Not a status update — a number, on a dashboard, that trends. A migration without one cannot be seen to have stopped.
  • A rule that stops new usage. A lint rule or review policy forbidding new callers of the old path, from day one. Otherwise the migration races against feature work and never converges.
  • Migrate what you are already touching. Dedicated migration time competes with features and loses. Work that rides along with other changes does not.

Check your understanding

Common mistakes

Starting without a commitment to finish. A permanent half-migration is worse than either system.

No visible progress number. A stall looks identical to slow progress.

No rule stopping new usage of the old path. The migration races feature work and never converges.

Two-way data synchronisation. Split by entity so each has one owner instead.

Migrating by endpoint when data spans endpoints. One entity ends up owned by two systems.

An abstraction shaped by the old implementation. The replacement inherits irrelevant constraints.

Skipping run-both-and-compare. The only realistic way to validate a replacement for poorly understood behaviour.

No deletion step. Flags, routes and abstractions become permanent complexity.

Practice

A 12-year-old monolith handles orders, invoicing, inventory and reporting. Leadership wants microservices. Design the first six months, and say what you would extract first and why.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

The first extraction should be chosen for what it teaches, not for what it is worth.

Show a strong answer

Push back on the framing first. "Microservices" is a solution, not a goal. I would want the actual problem stated — deployment frequency, team autonomy, scaling a specific component, or a specific part of the code being unmaintainable — because it determines what to extract and, occasionally, whether to extract anything at all. A monolith that is slow to deploy may need a faster pipeline rather than a distributed system.

Month one: build the interception point and measure. Nothing can be strangled without somewhere to route. A gateway in front of the monolith, initially passing everything through unchanged, is the prerequisite and delivers something immediately: per-endpoint traffic, latency and error rates. That data will contradict at least one assumption about what matters.

What I would extract first: reporting. Not because it is valuable — it is the least valuable of the four — but because it is the best teacher.

  • It is read-only. No dual-write problem, which is where these projects actually fail. The new service can read a replica.
  • Its failure is survivable. A broken report is embarrassing; a broken order path is an incident. The first extraction will have mistakes in it.
  • It exercises the whole path — gateway routing, deployment, monitoring, on-call, data access — so the team learns the mechanics on a low-risk surface.

Extracting orders first is the common instinct and it is the wrong one: it is the most valuable, most coupled and most write-heavy part of the system, so the team meets the hardest problem while still building the tooling.

Months two and three: reporting live, then invoicing. Invoicing is the natural second because it is relatively self-contained and its writes are append-mostly, which keeps the data question tractable.

Months four to six: orders, with the data ownership question settled before any code is written. Migrate by entity, not by endpoint — every endpoint touching orders moves together, so orders have exactly one owner throughout. If that cannot be arranged, the boundary is wrong and I would rather redraw it than accept two-way synchronisation.

What I would insist on throughout:

  • A visible count of endpoints remaining on the monolith, on a dashboard. This is the single most effective anti-stall measure.
  • A rule that no new functionality is added to the monolith — the strangler's version of stopping the bleeding.
  • Explicit deletion of migrated code. Leaving the old order handling in place "just in case" means the monolith never shrinks and the benefit never arrives.

What I would say to leadership at the outset: the first six months will produce one or two extracted services and a great deal of infrastructure, and the deployment-frequency benefit they are after will not be visible until the third or fourth. Setting that expectation early is what prevents the project being judged a failure at month four and abandoned half-done — which is the worst available outcome.

You must replace a pricing engine used from 40 call sites. It is poorly documented, its behaviour is business-critical, and it has no tests. Design the migration.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

You cannot write a specification for behaviour nobody knows. What can you use instead?

Show a strong answer

The absence of a specification is the central problem, and it determines the whole approach. Nobody can tell you what correct is, so the only available definition is what the current engine does — including its bugs, which downstream systems and customer expectations have probably absorbed.

Step one: introduce the abstraction, changing nothing. One interface over the pricing engine, all 40 call sites routed through it. No behaviour change, mechanical, reviewable. Two cautions:

  • Design the interface from the call sites, not from the engine. Read all 40 and find what callers actually need. An interface extracted from the engine's methods will encode its assumptions and constrain the replacement for no reason.
  • Expect the 40 to reduce. Call sites usually collapse into a handful of genuine use cases once you look, and knowing that shapes the interface correctly.

Step two: capture behaviour from production. With the abstraction in place, log inputs and outputs for real traffic. After a few weeks — including a month-end, a promotion, whatever the business's cycles are — you have a corpus that is a far better specification than anything anyone could write from memory. This corpus is the deliverable of this phase, and it is valuable even if the migration is later abandoned.

Step three: build the new engine and run both, comparing. Call both implementations, return the old result, log disagreements. This is the step that makes the migration possible at all:

  • Disagreements are the work queue. Each is either a bug in the new engine or a behaviour of the old one nobody knew about — and both need a decision by someone who owns pricing, not by an engineer guessing.
  • Some will be intentional differences, where the old engine is wrong. Those need explicit sign-off, because "fixing" a pricing bug changes what customers are charged.
  • Run until disagreements are zero or explained across a full business cycle. Not until they are rare — until each remaining one has a name.

Step four: switch gradually behind a flag. By percentage of traffic, or by lowest-risk product category first. Keep comparing in the background so a regression appears as a disagreement rather than as a customer complaint. Rollback is a flag flip.

Step five: delete the old engine, the comparison harness and the flag. With an owner and a date. This is the step that gets dropped, and leaving it means permanently running two pricing engines.

What I would not do: try to write tests for the old engine first. Forty call sites of undocumented business-critical behaviour cannot be specified from reading it, and tests written that way encode someone's guess. Production traffic is a better oracle, and it is available.

Key takeaways

  • Both patterns exist because big-bang rewrites fail reliably — no value until the end, and a scope discovered rather than known.
  • Strangler fig intercepts at a boundary and routes traffic old-or-new, one capability at a time. It needs an interception point.
  • Branch by abstraction creates a seam where no boundary exists, keeps everything on the main branch, and switches callers behind a flag.
  • The hard part of strangling is data ownership. Migrate by entity so each has one owner; avoid two-way synchronisation.
  • Run both and compare is the step that makes replacing poorly understood behaviour possible — production traffic is the specification nobody wrote.
  • Design the abstraction from what callers need, not from what the old implementation does.
  • Both stall unless you make them finish: a visible number, a rule stopping new usage, and migrating what you already touch.
  • Plan the deletion step, or the flags, routes and abstractions become permanent.

Further reading

  • Martin Fowler, "StranglerFigApplication" (2004).
  • Paul Hammant, "Branch by Abstraction" (2007).
  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — seams and characterisation tests.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019) — decomposition patterns and data ownership during migration.
  • Jez Humble and David Farley, Continuous Delivery (Addison-Wesley, 2010) — keeping trunk releasable during large changes.
  • Martin Fowler, "Feature Toggles" (2017).

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonFeature Toggles and Progressive Delivery
Lessons in this track