The Strangler Pattern A Solution
The strangler pattern replaces a legacy system without a risky big-bang cutover by inserting a facade (router/proxy) in front of it and rerouting one capability at a time to new services, so the old system keeps serving every request that has not yet been migrated and is deleted only once nothing routes to it anymore.
The name comes from the strangler fig, which grows around a host tree, gradually takes over its structure, and leaves a hollow shell where the original stood. Martin Fowler borrowed it in 2004 for the same shape of migration: the new system grows around the old one at the network edge, and risk is bounded to the single slice you are moving rather than the whole rewrite.
How the mechanism actually works
Every request first hits a facade — an API gateway, a reverse proxy (nginx/Envoy), or a thin routing service. The facade holds a routing table keyed by path (or header, or feature flag). A route is flipped from "monolith" to "new service" the moment that capability is live in the new system and its data is available there. Until then the default route sends the request to the legacy monolith untouched, so callers never see the migration.
The four moves, in order:
- Insert the facade first, routing 100% to the monolith. This is a no-op release that proves the proxy layer works before any behaviour changes.
- Carve one seam. Pick a capability with a clean boundary and light data coupling; build it as a new service.
- Flip its route (often behind a canary: 1% → 10% → 100%, with instant rollback by reverting the route).
- Repeat until the monolith serves nothing, then delete it — code, infra, and licence.
Worked example: strangling an e-commerce monolith
The monolith shop.example.com serves four capabilities from one codebase and one Postgres DB: /catalog, /cart, /reviews, /checkout. Traffic is ~2,000 req/s. We migrate over a series of sprints, flipping routes in the facade. Trace the routing state:
| Sprint | Facade route change | Goes to monolith | Goes to new service | Data reality |
|---|---|---|---|---|
| 0 | Insert facade, all → monolith | 100% | 0% | 1 shared DB |
| 3 | GET /reviews/* → reviews-svc (canary 1→100%) | ~93% | ~7% (reads) | reviews-svc reads a replica of the reviews table |
| 5 | POST /reviews → reviews-svc | ~90% | ~10% | reviews-svc now owns reviews; backfill + CDC feed to monolith so /catalog can still show avg rating |
| 9 | /catalog/* → catalog-svc | ~55% | ~45% | catalog-svc consumes rating events from reviews-svc instead of a SQL join |
| 14 | /cart, /checkout → new svcs; last route flipped | 0% | 100% | monolith DB frozen, then dropped; monolith deleted |
The instructive step is sprint 5. Once reviews-svc owns writes, the reviews table stops being the source of truth inside the monolith — but /catalog (still in the monolith) rendered each product's average star rating with a SQL join to that table. That join no longer exists across the service boundary. You must replace it: reviews-svc emits a ReviewPosted event (or you run change-data-capture), and the monolith maintains a local product_rating projection it can join to. This cross-boundary read is the dashed line in the diagram, and it is where most strangler migrations get hard.
The facade rule, and why the naive version is wrong
With nginx-style prefix locations, the longest matching prefix wins regardless of declaration order, so the specific migrated route beats the catch-all default:
# API gateway = the strangler facade
upstream reviews_svc { server 10.0.2.11:8080; }
upstream legacy_mono { server 10.0.1.10:8080; }
server {
listen 80;
# migrated capability — longest prefix, wins over "/"
location /reviews/ {
proxy_pass http://reviews_svc;
}
# default: everything not yet migrated
location / {
proxy_pass http://legacy_mono;
}
}
Why a naive hand-rolled router is wrong: if you instead iterate an ordered list and return the first matching prefix, putting the catch-all "/" before "/reviews/" silently sends every review request to the monolith — the migration looks deployed but no traffic ever reaches the new service. A prefix router must match by specificity (longest prefix), not list order, or the flip is a no-op that only surfaces when someone notices the new service has zero traffic.
Pitfalls a working engineer hits
- The shared database keeps the two coupled. The quick way to migrate a capability is to point the new service at the same tables the monolith uses. It ships fast, but you have not decomposed anything — neither side can change its schema, and a lock or migration on that table stalls both systems. Treat a shared DB as a temporary crutch with a deletion date, not an end state.
- Dual-write divergence. During transition you may write to both old and new stores. There is no transaction across two databases, so if the second write fails you get silent drift (a review saved in reviews-svc but missing from the monolith). Prefer one authoritative writer plus CDC/events over dual-write; if you must dual-write, add a reconciliation job.
- The forever-strangler. The first 70% is easy; the last 30% is the gnarly, high-risk core (billing, auth). Business urgency evaporates once the visible wins land, and the facade + monolith live on for years, doubling ops cost. Sequence the scary seams early or set a hard retirement deadline.
- Broken referential integrity. Splitting the reviews table away from products breaks the foreign key and any cross-table join. You trade a DB constraint for application-level consistency (events, projections) — plan for it, don't discover it in prod.
- Distributed transactions at the seam. A checkout that now spans migrated cart-svc and a still-legacy payment path can't use a single DB transaction. You need a saga with compensating actions, or you must keep both sides of that transaction on the same side of the seam until you're ready to move them together.
- Routing inconsistency under partial rollout. If a read is canaried to the new service but the write still goes to the old one, a user can post a review and not see it. Flip reads and writes for a capability together, or make the canary sticky per user/session.
When to use it — and when not to
Reach for the strangler when the system is large, business-critical, and cannot take downtime; you must keep shipping features during the migration; and you can find clean seams (HTTP/RPC or capability boundaries) to carve along. These are the concrete go-signals: a monolith too big to rewrite in one release, a revenue path you can't freeze, and identifiable modules with limited data entanglement.
Trade-offs versus a big-bang rewrite. The strangler's cost is real: you run and maintain two systems plus a facade, you build data-sync plumbing (events/CDC/dual-write) that is throwaway, and the intermediate architecture is uglier than either endpoint. A big-bang rewrite avoids all of that — no facade, no sync, a clean end state — but pays with high risk (one flawed cutover breaks everything), a long stretch delivering zero user value, and the second-system effect. Choose the strangler when the system is large and must stay live; prefer a big-bang rewrite when the codebase is small enough to re-build in a few weeks, or so entangled that building the facade and sync layer costs more than just rewriting it.
Versus branch-by-abstraction: that pattern does the same incremental replacement but inside a single deployable, behind a code-level abstraction, with no network facade. Prefer it when the seam is within one process/repo; prefer the strangler when the seam is across a network boundary between deployables.
Versus parallel run: parallel run executes both old and new implementations for every request and compares outputs to prove parity before cutover. Prefer it (often combined with the strangler) when correctness must be provably identical — pricing, billing, tax — and a subtle behavioural difference is unacceptable.
Takeaways
- The pattern's power is a per-slice risk bound: a facade lets you flip one capability at a time and roll back by reverting a route, so you're never one deploy away from breaking the whole system.
- The routing is the easy part; data ownership at the seam is the hard part — every cross-boundary join or foreign key you break must be replaced with events, CDC, or a projection.
- A shared database and dual-writes are transition crutches, not destinations; give them explicit deletion deadlines or you never actually decompose.
- Choose the strangler for large, live, seam-able systems; choose a big-bang rewrite when the thing is small or so tangled the facade+sync tax exceeds the rewrite; add a parallel run when parity must be proven.
Re-authored and deepened for this guide. Sources: Martin Fowler, "StranglerFigApplication" (martinfowler.com, 2004); Sam Newman, Monolith to Microservices (O'Reilly, 2019), chapters on the strangler fig, branch-by-abstraction, and parallel run; Microsoft Azure Architecture Center, "Strangler Fig pattern"; and nginx location matching semantics from the official nginx documentation.
🤖 Don't fully get this? Learn it with Claude
Stuck on The Strangler Pattern A Solution? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Progressively stronger hints — you still solve it.
I'm working on the problem **The Strangler Pattern A Solution** (System Design). Give me a HINT LADDER: start with the tiniest nudge, then wait. Only reveal the next, stronger hint when I ask. Do NOT show the full solution unless I type 'show solution'. Keep me doing the thinking. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
See the technique, not just code.
Explain the optimal approach to **The Strangler Pattern A Solution** with a VISUAL walkthrough: trace it on a small concrete example using ASCII art / a step-by-step diagram, narrate what changes each step, then give time & space complexity with a one-line derivation. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Catch bugs, edge cases, sub-optimality.
I'll paste my solution to **The Strangler Pattern A Solution**. Review it for correctness, missed edge cases, and time/space complexity, then coach me toward the optimal — don't just rewrite it. Ask me to paste my code now. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Lock in recognition with look-alikes.
Give me 2 problems that use the SAME underlying pattern as **The Strangler Pattern A Solution**. For each, let me attempt first, then review my answer and name the trigger signal that reveals the pattern. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.