What you'll learn
Quick Answer
The strangler fig pattern is a migration strategy where you put a routing layer in front of a legacy system and gradually redirect traffic to new components as you build them, one capability at a time, until the old system handles nothing and can be switched off. It avoids the all-or-nothing risk of a full rewrite by letting the legacy and new systems run side by side during the transition.
What the Strangler Fig Pattern Actually Is
A strangler fig is a real plant. Its seeds land in the canopy of a host tree, send roots down to the ground, and slowly wrap themselves around the trunk. Over years, the fig's own structure takes over completely, and the original tree — now fully enclosed — dies and rots away, leaving the fig standing in its place. Martin Fowler borrowed this image in 2004 to describe a specific way of replacing software: instead of tearing the old tree out and planting a new one (a rewrite), you grow the replacement around it, one branch at a time, until nothing is left of the original.
In practice, this means you put a routing layer — Fowler called it a strangler facade — in front of your legacy system. Every request that used to go straight to the old system now goes through this facade first. Initially, the facade forwards everything to the legacy system unchanged, because nothing has been migrated yet. As you build new versions of individual capabilities, you update the facade to send just those specific requests to the new code, while everything else keeps flowing to the legacy system exactly as before. Capability by capability, the share of traffic hitting the old system shrinks, until one day it's handling nothing at all and you can delete it.
The pattern is deliberately boring at any single point in time. At no point are you running a risky full cutover. At every point, the system in production is a hybrid of old and new, and that hybrid is always in a working, releasable state. That property — always shippable, never a big dangerous switch — is the entire reason teams choose it.
How the Routing Facade Works
The facade is usually the cheapest, most boring piece of infrastructure you already have: a reverse proxy, an API gateway, or a router inside your own application code. What matters isn't the specific technology — it's that every request has to pass through a single decision point before it reaches either system.
Say you're migrating an old monolithic order-management system. You've decided the first capability to extract is order search, because it's read-only, low-risk, and doesn't touch the checkout flow. A reverse proxy in front of both systems might look like this:
http {
upstream legacy_monolith {
server monolith.internal:8080;
}
upstream new_search_service {
server search-service.internal:9090;
}
server {
listen 80;
location /orders/search {
proxy_pass http://new_search_service;
}
location / {
proxy_pass http://legacy_monolith;
}
}
}
Nginx matches the most specific location block first, so any request to /orders/search is routed to the new service, and literally everything else — every other URL in the application — still goes to the legacy monolith, completely untouched. Nobody using the rest of the system even knows a migration is happening. As you extract the next capability, say order cancellation, you add one more location block and repeat. The facade grows a little with each migrated slice; the legacy system shrinks by exactly the same amount.
Routing doesn't have to be by URL path. Teams also route by HTTP header, by user ID (migrate one customer segment at a time), or by feature flag evaluated inside the facade itself. The mechanism is interchangeable — what's constant is that the decision of "old system or new system" is made in exactly one place, so you never have two different parts of your infrastructure disagreeing about which system owns a request.
A Worked Example: Migrating One Capability at a Time
Picture a 12-year-old PHP monolith that handles an e-commerce company's entire backend: catalog, search, checkout, order history, admin tools, all one codebase, one database, one deploy. Nobody wants to touch it, and everyone agrees a full rewrite would take 18 months and probably fail halfway through. Here's how a strangler migration of just the search capability might actually run:
- Inventory the capability's real behavior. Before writing a line of new code, the team reads the legacy search code closely enough to document every edge case it handles — typo tolerance, out-of-stock filtering, category ranking rules — because the new service has to match this behavior or customers will notice regressions.
- Build the new service against a read replica. The new search service is built independently, reading from a replica of the same production database so it doesn't need the legacy system's cooperation to have real data.
- Shadow traffic before routing it. Before the facade sends any real user to the new service, it duplicates a copy of live search requests to the new service in the background, compares the two systems' responses, and logs the differences without showing anything to users. This catches behavioral gaps for free, with zero customer risk.
- Cut over gradually. Once shadow results look clean, the facade starts routing a small percentage of real search traffic to the new service — the same weighted-routing mechanics used in canary releases — while watching error rates and latency.
- Ratchet to 100%, then delete the old code path. Once the new service has handled all search traffic reliably for a period the team is comfortable with, the legacy monolith's search module is deleted outright — not commented out, not left "just in case." Dead code with no traffic is a liability, not a safety net.
Multiply this by every capability in the monolith — checkout, catalog, order history, admin — and you get a migration that takes months or years but never requires a single day where the whole system is down or unproven.
Why Teams Choose This Over a Full Rewrite
The alternative to a strangler migration is the "big rewrite": freeze the legacy system, build a replacement from scratch, and cut over on a single day. Joel Spolsky's oft-cited essay on Netscape's decision to rewrite its browser from the ground up in 1999 is the canonical cautionary tale — Netscape stopped shipping meaningful updates for roughly three years while the rewrite was underway, and by the time it shipped, competitors had taken the market. The core problem isn't that rewrites are impossible; it's that they take longer than estimated, and every month spent rewriting is a month the old system's business logic — accumulated through years of edge cases, regulatory fixes, and customer-specific workarounds — has to be rediscovered and reimplemented from a spec that usually doesn't fully exist anywhere except the legacy code itself.
A strangler migration sidesteps this in three concrete ways. First, the system in production is never frozen — the legacy system keeps shipping bug fixes and small features for the capabilities that haven't migrated yet, so the business doesn't stop moving for years. Second, correctness is validated incrementally and in production, against real traffic, capability by capability, instead of in one enormous integration test suite for a system nobody has run against real users yet. Third, and most importantly, the blast radius of a mistake is tiny: if the new order-search service has a bug, you flip the facade back to the legacy system for that one route, and the rest of the application is completely unaffected. A big-bang rewrite has no equivalent partial rollback — you either accept the new system's bugs in production or roll back everything, including the parts that worked.
The Hard Parts Nobody Mentions
The pattern is easy to describe and genuinely hard to execute, mostly because of one thing: shared data. Routing HTTP requests is the easy 20% of a strangler migration. The hard 80% is that the legacy system and the new system usually need to read and write the same underlying data during the transition, and databases don't have a "location block" to route around.
If the new order-search service and the legacy monolith are both reading from the same orders table, that's usually fine — reads are easy to duplicate. The moment a migrated capability needs to write data that the still-legacy parts of the system also depend on, you're in dual-write territory: either both systems write to the same database and you carefully coordinate schema changes so neither one breaks, or the new system owns its own data store and you build a synchronization mechanism (change-data-capture, an event stream, or a dual-write shim) to keep the legacy system's view consistent. Get this wrong and you get the worst possible outcome: silent data drift that nobody notices until a customer complains that their order history doesn't match their invoice.
Two other traps show up constantly in real migrations:
- The facade becomes permanent. Teams build the routing layer as a "temporary" piece of migration infrastructure, then never get funded to remove it once the migration is "done enough." Five years later, it's still there, nobody remembers why half its routing rules exist, and it's effectively become a second legacy system.
- Nobody defines "done." Without an explicit list of capabilities to migrate and an owner for deleting each legacy code path once it's fully replaced, strangler migrations tend to stall at 70-80% complete indefinitely — the easy, low-risk capabilities get migrated first, and the gnarliest, highest-value ones (usually billing, or anything touching money) get pushed back forever because nobody wants to own that risk.
A Practical Checklist for Running One
If you're about to start one of these migrations, a few decisions upfront save months of pain later:
- Inventory capabilities, not code files. List what the system does for users and the business, not its internal module structure — you're migrating behavior, and behavior boundaries rarely match file boundaries in a legacy codebase.
- Migrate low-risk, high-visibility capabilities first. Something read-only and easy to verify (like search) builds confidence and proves the facade pattern works before you touch anything that moves money.
- Decide the data strategy before writing new-system code. Will the new service read from a replica, subscribe to change events, or own its own store? This decision is much more expensive to reverse than any routing rule.
- Instrument both systems identically. You need directly comparable latency, error-rate, and business metrics (orders placed, searches returning zero results) from both the legacy and new code paths, or you're migrating blind.
- Put an explicit owner and deadline on deleting each legacy code path. "Migrated" means the old code is deleted, not dormant. A capability isn't done until its legacy implementation no longer exists.
