The Problem Legacy Systems
The mechanism
A routing facade — a reverse proxy placed in front of the legacy system — intercepts every incoming request and forwards a chosen subset of URL paths to newly built services while everything else still hits the old monolith, so the replacement grows around the original and slowly starves it of traffic until nothing calls it and it can be deleted.
That indirection exists because a legacy system is rarely just bad code you can retype. It runs critical operations at real volume, usually shares one database, and carries unknown unknowns — undocumented behaviour some customer quietly depends on. A big-bang rewrite that flips everyone onto a new system on a Friday night has no incremental rollback: one missed edge case is a full outage, and the whole quarter's rewrite is bet on a single switch. The facade turns that one terrifying switch into hundreds of small, individually reversible ones.
The name is Martin Fowler's, after the strangler fig (a Ficus): it germinates in a host tree's canopy, sends roots down around the trunk, and over years the host rots away, leaving a self-supporting fig in its shape. The new application envelops the old one the same way — the facade is the trunk it grows down.
Worked example: peeling the product catalog off a shop monolith
shop.example.com is one deployable, shop-monolith, serving about 2,000 req/s across six endpoints. Browsing the catalog (GET /products) is roughly 1,200 req/s — 60% of traffic and almost all reads. That makes it the safest first slice to peel: high volume (so you learn fast) but low risk (a stale product page hurts far less than a corrupted order).
We put a reverse proxy in front and build a small catalog-svc beside the monolith. The facade config routes only /products, and canaries it at 5% by weighting a shared pool 1:19:
# Two real backends behind the facade
upstream monolith { server 10.0.0.2:8080; }
upstream catalog_svc { server 10.0.1.10:8080; }
# Canary pool: weight 1:19 sends ~5% of /products to the new service
upstream catalog_canary {
server 10.0.1.10:8080 weight=1; # catalog-svc (new)
server 10.0.0.2:8080 weight=19; # monolith (still authoritative)
}
server {
listen 80;
server_name shop.example.com;
# Migrated slice. Ramp = point this at catalog_svc; roll back = monolith.
location /products { proxy_pass http://catalog_canary; }
# Default: everything not yet peeled off stays on the legacy app.
location / { proxy_pass http://monolith; }
}To ramp, change one line — point location /products at catalog_svc. To roll back, point it at monolith. Neither backend is redeployed; the migration lives entirely in the facade's config.
The migration, phase by phase
Each phase changes one facade rule and is reversible in under a minute. Loads are steady-state approximations at 2,000 req/s total.
| Phase | Facade change | catalog-svc load | monolith load | Roll back by… |
|---|---|---|---|---|
| 0 · Baseline | install facade, pure passthrough | 0 req/s | 2,000 req/s | removing the facade |
| 1 · Shadow | mirror GET /products to catalog-svc, discard its reply, diff the two responses in logs | ~1,200 req/s (mirrored; users never see it) | 2,000 req/s (authoritative) | deleting the mirror directive |
| 2 · Canary 5% | weight pool 1:19 | ~60 req/s | ~1,940 req/s | setting weight to 0 / repointing |
| 3 · Ramp reads | /products → catalog_svc | ~1,200 req/s | ~800 req/s (40%) | repointing /products to monolith |
| 4 · Next slice | add /cart → cart-svc | +~300 req/s (cart) | ~500 req/s | repointing /cart |
| N · Retire | no location points at monolith | 2,000 req/s across new services | 0 req/s | keep the image 30 days, then delete |
Notice the shape: reads move first (phases 1–3) because they are idempotent and cheap to mirror and diff; writes and the harder endpoints come later, once the new store has proven it returns the same answers.
Pitfalls
Why the naive version is wrong. The easy read of this pattern is "route HTTP paths." But if catalog-svc and the monolith both read and write the same products table, you have split nothing: the two now race on the same rows, dual writes silently clobber each other, and you own a distributed monolith with none of the isolation you paid for. The path split is the visible 10%; the real work is splitting the data — give the new service its own store and sync it via change-data-capture or events, or run a shared-DB step first and cut the seam deliberately. Route writes to the new service only once its store is authoritative.
- The facade becomes the system. It is now a single point of failure on the request path and a magnet for "just one bit of business logic." Keep it dumb (routing only), make it highly available, and instrument every route — a broken facade takes down old and new at once.
- Reads and writes move together by accident. Naively routing
/productssendsPOST /productsto the new service too, before its store is authoritative → lost updates. Split by method, not just path, until the new store owns the data. - The never-ending migration. The last 5% of gnarly endpoints — the unknown unknowns — stall, and both systems run forever, doubling infra and cognitive load. Set a retire date up front and cut the tail deliberately; a strangler with no deadline is just two monoliths.
- Session and auth affinity. If the legacy holds sticky in-memory sessions and the facade splits one user's requests across both backends, session state diverges mid-checkout. Share a session store, or route whole users (not whole paths) during transition.
When to use it — and when not
Reach for Strangler Fig when the system is large and business-critical, a big-bang cutover is unacceptable, you can identify seams (endpoints or bounded contexts) that peel off cleanly, and you need to keep shipping features throughout. It shines precisely when behaviour is partly undocumented, because the shadow phase lets you verify each slice against real traffic before you trust it.
Do not reach for it when the system is small enough that a full rewrite fits in one release cycle (the facade and the long dual-run tax cost more than they save), when the seams simply do not exist (one giant tangled transaction that touches everything — you cannot peel a slice), or when the legacy is being decommissioned entirely because its users are leaving (just freeze it).
Trade-offs vs a big-bang rewrite. Strangler buys incremental rollback, continuous delivery, early feedback, and a small blast radius. It costs you running both systems for months or years — double infra, double the mental model — plus building and operating the facade and the data-sync glue. A big-bang rewrite buys a clean-slate design and no dual-run tax, but bets the business on one cutover where a single defect is a full outage (the graveyard of failed rewrites). Choose Strangler when downtime is unacceptable and the system is large; prefer big-bang when the system is small, well-understood, and a short freeze is fine.
Trade-offs vs an anti-corruption layer (encapsulate and leave). Here you wrap the legacy behind a clean API and stop — no replacement. It is the cheapest option with near-zero migration risk, but every underlying problem (scaling ceiling, security posture, nobody left who knows COBOL) survives untouched. Choose this when the legacy actually works and only its edges hurt; choose Strangler when the core itself must go. Note that parallel run — serving both, comparing outputs, trusting the old one until confident — is not really a competitor: it is the shadow phase inside a strangler migration.
Takeaways
- The pattern is a routing facade plus incremental slices — the facade is what lets you shift and roll back traffic one path at a time, without redeploying either system.
- Move reads before writes, and split the data, not just the URLs. A shared database means you decoupled nothing.
- The dominant cost is running two systems at once; defend against it with a hard retire date and by killing the long tail on purpose.
- It trades one big, risky cutover for a long, low-risk migration — worth it only when the system is large and cannot go down.
Sources: Martin Fowler, "StranglerFigApplication" (martinfowler.com, 2004; renamed 2019); Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 3 on the strangler fig, parallel run, and branch by abstraction; Chris Richardson, microservices.io — "Strangler Application" pattern. Re-authored and deepened for this guide, with a worked NGINX facade example.
🤖 Don't fully get this? Learn it with Claude
Stuck on The Problem Legacy Systems? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **The Problem Legacy Systems** (System Design) and want to truly understand it. Explain The Problem Legacy Systems from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **The Problem Legacy Systems** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **The Problem Legacy Systems** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **The Problem Legacy Systems** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.