All articles
Design PatternsSystem DesignArchitecture13 min read

Strangler Fig: Replacing a Monolith Without a Big-Bang Rewrite

Rewrites fail because they require you to be right about everything at once. The strangler fig pattern migrates one route at a time behind a façade, with dual writes, shadow traffic and a real rollback path.

Why rewrites fail

The plan is always the same: build the new system alongside the old one, then switch. It fails for structural reasons, not for lack of effort.

  • The old system keeps changing, so the target moves for the entire duration.
  • Undocumented behaviour is discovered only in production, after cutover.
  • No value ships until the very end, so the project is politically fragile.
  • Rollback means reverting months of work — which means there is no rollback.
The name
The strangler fig germinates in a host tree's canopy, sends roots down around the trunk, and gradually takes over its structural role. The host is eventually gone — but the forest never had a gap.

The strangler fig idea

Put an interception layer in front of the monolith. Move one capability at a time behind it. Each move is small, independently shippable and independently revertible. The monolith shrinks until deleting it is uneventful.

Phase 10% new servicesPhase 230% new servicesPhase 365% new servicesPhase 4100% new servicesaccent = new services · muted = monolith · the façade routes both throughout
Every phase is a working production system. That is the entire advantage.

Step 1: put a façade in front

Before migrating anything, route 100% of traffic through a proxy or gateway that currently forwards everything to the monolith. This changes nothing functionally and is therefore safe to ship on a Tuesday.

routes:
  - path: /*        -> monolith          # everything, for now

# later, one line at a time:
  - path: /products/*  -> catalog-svc
  - path: /search/*    -> search-svc
  - path: /*           -> monolith
A façade with no migrations yet — the boring, essential first deploy.

You now own the routing decision. Without this layer, every migration requires client changes, and clients you do not control (old mobile builds) will never change.

Step 2: choose the first slice

The first slice sets the tone for the whole programme, so pick for learning value, not for impressiveness.

PropertyGood first sliceBad first slice
CouplingFew reads of shared tablesJoins across half the schema
RiskRead-heavy, degrades gracefullyPayment capture
ClarityObvious bounded context'Utils' or 'core'
SizeShips in weeksShips in quarters
ValueSomething that is currently painfulSomething nobody uses

Product catalogue reads, search, notifications and reporting are common first choices. Do not start with the ledger.

Step 3: the hard part is the data

Splitting code is a refactor. Splitting data is the migration. Options, in increasing order of independence:

  1. Shared database, new service. Fastest, and a trap if you stop here — you have a distributed monolith with two deploy pipelines and one schema.
  2. New service owns writes, monolith reads a replica or view. Clear ownership, read-only coupling.
  3. Dual write during transition. Both stores updated, reads from one, with a continuous reconciliation job comparing them.
  4. CDC-fed sync. Change data capture keeps the new store current without application dual writes; usually the cleanest for large tables.
// nightly, and after every deploy
const drift = await compare({
  left:  monolith.products.checksumByPage(),
  right: catalogSvc.products.checksumByPage(),
});
if (drift.length) alert("catalog drift", { pages: drift.length });
Reconciliation is not optional — dual writes drift, always.

Step 4: shadow, canary, cut over

  1. Shadow traffic. Send a copy of production reads to the new service, discard its response, and diff it against the monolith's. This finds the undocumented behaviour before users do.
  2. Canary. Route 1% → 5% → 25% → 100%, watching error rate and latency at each step.
  3. Keep the flag. The route switch must be a config change that reverts in seconds, not a redeploy.
  4. Hold the old path warm for at least one full business cycle — month-end reveals code paths nothing else does.
1Façade2Build service3Shadow diff4Canary5Cut over6Delete old
The verification ladder for a single migrated capability.

Step 5: actually delete the old code

This is the step teams skip, and skipping it is what turns a strangler migration into permanent dual maintenance. Once traffic has been at 100% for a cycle:

  • Delete the monolith's implementation, not just its route.
  • Drop the abandoned tables — after a verified backup and a waiting period.
  • Remove the dual-write code and the reconciliation job.
  • Record the date. Sequencing evidence of completed slices is what keeps funding alive.

Pitfalls

  • The distributed monolith. Services that must deploy together are worse than the monolith, because now you have network calls too.
  • Migration fatigue. Three slices in, momentum dies and you live in both worlds forever. Timebox and sequence explicitly.
  • Rewriting behaviour while migrating. Move first, improve second. Changing both at once makes every diff ambiguous.
  • No façade. Without the routing layer, you are doing a rewrite with extra steps.

Interview framing

“First I put a routing façade in front of the monolith with no behaviour change. Then I take catalogue reads as the first slice — low coupling, read-heavy, degrades gracefully. The new service is fed by CDC initially so the monolith stays authoritative, I shadow production reads and diff responses, then canary 1% to 100% behind a flag that reverts in seconds. Once it has survived a month-end I flip write ownership and delete the monolith's implementation.”

Keep reading

Suggested next articles based on this one.

Design it, don't just read it.

Practise LLD and system design problems with structured rubrics and AI feedback.

Start practising free