The Strangler Fig Pattern: Replacing a Legacy System Without a Rewrite
Joel Spolsky called the decision to rewrite Netscape Navigator from scratch “the single worst strategic mistake” a software company can make. Netscape shipped nothing for three years while the rewrite crawled and Internet Explorer took the market. Twenty five years later, teams still greenlight big-bang rewrites of systems that are too important to stop and too tangled to freeze.
The strangler fig pattern is the alternative: replace the legacy system slice by slice, in production, while it keeps running. Martin Fowler named it in his strangler fig application ↗ note after the tropical fig that grows around a host tree until the host dies away inside it. The name is memorable. The mechanics are what matter, and they are more specific than “migrate gradually”.
The Three Moving Parts
Every strangler fig migration has the same anatomy, whatever the stack:
- A facade that owns routing. Every request to the legacy system now passes through a layer you control: an API gateway, a reverse proxy, an nginx config, a load balancer rule, or a branch inside the monolith itself. This is the intercept point. Without it there is no pattern, just two systems and hope.
- New implementations of one slice at a time. A slice is a vertical piece of behaviour: one endpoint, one page, one workflow. Not “the data layer”. Horizontal slices cannot be routed independently, so they cannot be shipped independently.
- A retirement ratchet. When a slice is fully served by the new code, the legacy code for that slice is deleted. Traffic never flows backwards.
How a Migration Actually Runs
Step 1: Get the facade in with zero behaviour change
The first deliverable routes 100 percent of traffic to the legacy system through the new intercept point. It sounds like a non-event. It is the foundation of everything, and it flushes out the real-world surprises early: the batch job that bypasses the API, the partner integration hard-coded to an internal hostname, the cron script nobody owns.
Ship this alone and let it soak. If the facade adds latency or drops edge-case requests, you want to learn that while it is still a transparent proxy.
Step 2: Pick the first slice for learning, not value
The instinct is to start with the highest-value slice. Start instead with a slice that is real but forgiving: read-only, low blast radius, well understood. The first slice exists to build the machinery: the routing rule, the deployment path, the comparison tooling, the rollback drill. You will get that machinery wrong once. Better on the product catalogue page than on payments.
Knowing which slices are load bearing and which are forgiving is exactly what a codebase survey gives you, which is why how to audit a legacy codebase is the step before this one.
Step 3: Route by cohort behind a flag
Do not flip a slice from 0 to 100. Route 1 percent of traffic, or internal staff only, or one tenant, and widen from there. This is the same progressive delivery muscle as canary deployments, applied at the architecture level, and feature flags are the natural switch. The flag must fail towards legacy: if the flag service is down or the new code errors, traffic falls back to the old path automatically.
Step 4: Run both paths and compare
For any slice that matters, run a parallel phase: send the request to both implementations, serve the legacy response, and log a diff of the two results. Shopify’s engineering team used exactly this verifier approach while strangling parts of their core monolith, described in their write-up on refactoring legacy code with the strangler fig pattern ↗.
The diff log is where the legacy system’s undocumented rules surface. Expect discrepancies, and expect a good share of them to be legacy bugs you must now consciously decide to preserve or fix. Preserve first, fix later, in separate changes: the same discipline as refactoring legacy code without breaking it.
Step 5: Ratchet and delete
When a slice holds at 100 percent on the new path for an agreed soak period, delete the legacy code for that slice in the same sprint. Not “comment it out”. Not “keep it just in case”. Every slice that stays deletable-but-not-deleted is a slice someone will accidentally change during the next urgent fix, and the diff between the systems starts growing again.
One more rule makes the ratchet stick: no new features land on the legacy path, ever. New behaviour goes into the new system, even when it would be quicker to bolt onto the old one. The moment you break this rule the migration stops converging.
The Database Is the Hard Part
Code routes cleanly. Data does not. The pragmatic sequence is:
| Phase | New code | Source of truth |
|---|---|---|
| 1. Shared database | Reads and writes the legacy schema directly | Legacy database |
| 2. Dual write | Writes to both stores, reads from legacy | Legacy database |
| 3. Backfill and verify | Historic data copied, diffs reconciled | Legacy database |
| 4. Read cutover | Reads from the new store | New database |
| 5. Contract | Legacy tables dropped | New database |
Phase 1 offends people. The shiny new service talking straight to the crusty old schema feels like a compromise, and it is: the correct one. It keeps one source of truth while the behaviour migrates, and it defers the riskiest step, splitting data ownership, until the slice is proven. Each later phase is a small, reversible migration of the kind covered in database migrations without the fear.
Strangler Fig vs the Alternatives
| Concern | Big-bang rewrite | Refactor in place | Strangler fig |
|---|---|---|---|
| Value before completion | None | Continuous | Continuous |
| Cutover risk | One huge event | None | Many tiny events |
| Can change stack or language | Yes | No | Yes |
| Can pause halfway safely | No | Yes | Yes |
| Runs two systems for a while | Yes, hidden | No | Yes, visible |
| Dies when | Funding or patience runs out | Design is fundamentally wrong | Retirement is never finished |
The middle column is underrated. If the legacy system’s design is broadly right and the problem is local mess, you do not need a strangler fig at all: method-level techniques like those in breaking dependencies in legacy code are cheaper. The strangler fig earns its overhead when the design, stack, or service boundaries themselves have to change.
Failure Modes to Design Against
The eternal strangle. The team migrates the easy 60 percent, the pain drops below the threshold that gets projects funded, and the organisation runs two systems forever. Defend against it from day one: retirement is a tracked deliverable with an owner and a date, and the slice list is finite and visible. A strangler fig without a kill date is just a second system.
The facade becomes the monolith. Routing layers attract logic. First a header tweak, then a fallback, then a bit of business logic that “has to live somewhere”. Two years later the facade is the new legacy. Keep it dumb: route, translate, fail over, nothing else.
Slicing horizontally. “First we will replace the data access layer” is not a strangler fig, because no user-visible behaviour ever moves and nothing can be routed or retired independently. Slices must be vertical.
Freezing the old system by decree. Announcing a code freeze on the legacy system while the migration runs does not work; urgent fixes will land anyway, silently. Instead, make changes to the legacy path expensive and visible: they require sign-off, and they must be mirrored in the new implementation before the slice ships.
For the wider team-level practices around living inside a system while you replace it, the developer’s guide to working with legacy code covers the human side of the same work.
The Short Version
- Put a facade you control in front of the legacy system, and ship it doing nothing before it does anything.
- Migrate vertical slices, routed by cohort behind flags that fail towards legacy.
- Parallel-run important slices and diff the outputs; the diffs are the spec nobody wrote down.
- Move data ownership last, through dual writes and a verified backfill.
- Ratchet hard: delete migrated legacy code immediately, never add features to the old path, and give retirement an owner and a date.
The strangler fig does not make replacing a legacy system fast. It makes the replacement survivable: always shippable, always reversible, and delivering value from the first slice instead of after the last one.
Frequently asked questions
What is the strangler fig pattern?
The strangler fig pattern is a technique for replacing a legacy system incrementally instead of rewriting it in one go. You place a routing layer, usually called a facade, in front of the old system, build a new implementation of one slice of functionality, and route a small share of traffic to it. As the new implementation proves itself, you route more traffic and build more slices, until the legacy system handles nothing and can be switched off. Martin Fowler named it after the strangler fig tree, which grows around a host tree until the host is no longer needed.
Why do big-bang rewrites fail so often?
Because the old system keeps moving while the new one is being built. A rewrite has to reimplement years of accumulated business rules, many of which exist only as code, while also keeping pace with new requirements landing in the old system. The new system delivers zero value until the final cutover, so the project is easy to cancel, and the cutover itself is a single high-risk event with no rehearsal. The strangler fig avoids all three problems by shipping the replacement in slices that go live as they are finished.
How long does a strangler fig migration take?
Longer than the optimistic rewrite estimate and shorter than the realistic one. Slices ship in weeks, so value arrives early even if full retirement takes a year or more. The honest answer is that the duration matters less than the shape: with a strangler fig the system is fully working at every point, each slice is individually shippable and revertible, and the business can pause the migration at any moment without being left stranded between two half-finished systems.
What is the biggest risk with the strangler fig pattern?
Stopping halfway. The most common failure is the eternal strangle: the team migrates the easy 60 percent, the pressure lifts, and the organisation now operates two systems forever, which costs more than either one alone. The defence is a ratchet: once a slice moves to the new path, the old path for that slice is deleted, not left as a fallback, and no new features are ever added to the legacy side. Retirement has to be a tracked deliverable with an owner, not an aspiration.
How do you handle the database during a strangler fig migration?
Data ownership moves last. Early slices of the new system usually read and write the legacy database directly, which is ugly but keeps one source of truth. Once a slice fully owns its behaviour, you migrate its data into its own store using an expand and contract approach: write to both stores, backfill, verify, then cut reads over. Trying to split the database on day one couples the riskiest part of the migration to the start, when you know the least.
Enjoyed this article? Get more developer tips straight to your inbox.
Comments
Join the conversation. Share your experience or ask a question below.
No comments yet. Be the first to share your thoughts.