The two-year rewrite is the most reliably failing project shape in enterprise software. Not because the engineers are wrong about the old system being bad, but because the old system keeps receiving changes the whole time, the new one must reach feature parity with a moving target, and nothing ships until everything does.
The strangler fig pattern replaces that with incremental substitution: put a routing layer in front, move one capability at a time behind it, and let the old system shrink until it is empty. Every step is in production, reversible, and delivering value before the project is finished.
The seam is the whole design
You need a point where traffic can be routed per capability — a reverse proxy, a gateway, or an application-level facade. This is the first thing to build and the thing to get right, because everything afterwards depends on being able to move a slice of traffic without a deploy of either system.
# The seam: one place that decides which system serves which capability.
upstream legacy { server legacy-app:8080; }
upstream modern { server modern-api:8000; }
map $request_uri $backend {
default legacy; # everything not yet migrated
~^/api/v1/invoices modern; # migrated, fully cut over
~^/api/v1/inspections modern;
}
server {
location / {
proxy_pass http://$backend;
proxy_set_header X-Migration-Route $backend; # visible in every log line
}
# Percentage cutover for the slice currently in flight, flag-controlled.
location /api/v1/reports {
set $target legacy;
if ($cookie_canary = "reports-modern") { set $target modern; }
proxy_pass http://$target;
}
}Dual-run before you cut over
For anything with non-trivial logic, the safest intermediate step is running both implementations and comparing. Send the request to the legacy system, return its answer, and asynchronously send the same input to the new one and record whether the outputs match. You accumulate real-traffic evidence with zero user risk, and the mismatches are a specification of the behaviour nobody documented.
This is where most of the value of the pattern actually lives. Every legacy system has undocumented behaviour that someone depends on, and comparison against production traffic is the only reliable way to find it. Expect the first week of mismatches to be genuinely educational.
Choosing the first slice
- Pick something with a clean data boundary. If the new component needs to write tables the legacy system also writes, you have not found a seam.
- Prefer read-heavy capabilities first; they are reversible and comparison is easy.
- Avoid starting with the most painful module. The first slice should prove the pattern, not test it to destruction.
- Write the deletion into the plan. A migration where the old path is never removed leaves two systems and twice the maintenance.
- Keep a dated inventory of what has moved. Without one, 'nearly done' becomes a permanent status.
A rewrite is a bet that you can hit a moving target while blindfolded. A strangler migration is a series of small bets you can stop making at any point.
The organisational advantage is what usually sells it. A two-year rewrite requires sustained faith from people who see no benefit until the end, and that faith rarely survives a change of leadership or a bad quarter. Incremental replacement delivers something every month, which means it survives the budget review that kills the big-bang version.