Strangler-fig modernization without freezing the business
How to replace a legacy system in slices while it keeps running, which slice to take first, and the three conditions that make the pattern fail in practice.
Every legacy replacement starts with the same proposal: freeze the old system, build the new one, cut over on a weekend. It almost never survives contact with a business that has to keep selling on Monday.
The strangler-fig pattern is the alternative. You put a routing layer in front of the legacy system, move one capability at a time behind it, and let the old system shrink until there is nothing left worth running. The name comes from the fig that grows around a host tree and eventually stands on its own.
The pattern is well known. What is less discussed is which slice to take first, and the conditions under which it quietly fails.
The routing layer is the whole trick
Nothing works until requests can be sent to either system without the caller knowing. That means a facade in front of the legacy application: a reverse proxy, an API gateway, or a thin service that owns the public contract.
Two properties matter more than the technology you pick:
- Routing is per-capability, not per-user. Routing a percentage of users to the new system means both systems own the same data at the same time. That is a data reconciliation project wearing a migration costume.
- The facade owns the contract. If clients still call the legacy system directly for anything, you cannot retire it. Every direct caller you miss becomes a reason the old system stays alive for another year.
Which slice to take first
The instinct is to start with the least risky thing. That is usually wrong, because the least risky thing also proves the least.
Rank candidate slices on four axes. The first slice should score well on all four, not brilliantly on one.
| Axis | What you want first | Why |
|---|---|---|
| Read/write ratio | Read-heavy | A read path can run in both systems and be compared without corrupting anything. |
| Data ownership | Owns its own tables | Shared write access to the same tables from two codebases is the failure mode that ends these projects. |
| Business visibility | Visible, not critical | Invisible wins do not buy you the runway for slice two. Critical ones make the first failure fatal. |
| Change frequency | Actively changing | A capability nobody touches has no forcing function. It will sit half-migrated forever. |
In practice this often means reporting, search, notifications, or a customer-facing read view. It rarely means billing, and it almost never means authentication.
Run both, compare, then switch
For a read path, the safest cutover is not a switch at all. It is a period where both systems answer and only one response is served.
- The facade sends the request to the legacy system and returns its response.
- In parallel, it sends the same request to the new service and discards the response.
- It records where the two disagree.
- When disagreement drops to zero or to a set of differences you have explicitly accepted, you flip which response is returned.
This costs you a duplicate call per request and buys you something no test suite gives: proof against real production traffic, including the inputs nobody documented.
The three conditions that make it fail
The pattern does not fail because of technology. It fails for three reasons, and you can check for all three before writing any code.
No one owns retiring the old system. If the migration budget is measured in features delivered, the last 20% of the legacy system never goes away, because the remaining slices are the ugly ones. Now you run two systems forever and pay for both. Make the retirement of a named legacy component the completion criterion of each slice, not the delivery of the new one.
The database is shared and stays shared. Two applications writing the same tables is not an intermediate state, it is a permanent coupling that both sides will build on. If the slice cannot own its data, either pick a different slice or accept an anticorruption layer as the real deliverable and stop calling it a migration.
The team that knows the legacy system is not involved. Undocumented behavior is the majority of what a twenty-year-old system does. The people who know which flags matter are the ones being replaced by the project, which is a governance problem before it is an engineering one. Budget their time explicitly, and pay for it.
What to measure
Three numbers tell you whether the migration is progressing or just accumulating code:
- Legacy endpoints still reachable directly, bypassing the facade. This should trend to zero. If it does not, nothing can ever be retired.
- Legacy components fully retired, not "migrated". Migrated means the new one exists. Retired means the old one is deleted.
- Slices in flight, which should be one or two. Five slices at 70% is worse than one at 100%, because each unfinished slice keeps a legacy dependency alive.
If those three numbers are not on a dashboard someone reads monthly, the migration is running on optimism.
When not to use it
The pattern earns its cost when the legacy system must keep running and the replacement will take longer than a quarter. If the system is small enough to rewrite in six weeks, the facade, the shadow comparison, and the dual operation are pure overhead. Rewrite it and cut over.
It also does not apply when the problem is the data model rather than the code. Wrapping a bad schema in a new service produces a new service with a bad schema. That is a data migration with an application project attached, and it should be planned as one.
We work this way on modernization engagements, and the same rules about data ownership and access show up in our engineering and security practices.