The Strangler Pattern for Legacy Code, Done Right
How to replace legacy code without a rewrite. The strangler pattern lets you route new work to new code and retire the old system piece by piece, safely.
Do not rewrite the legacy system. Strangle it. The strangler pattern replaces old code the way a strangler fig replaces a tree: it grows around the existing thing, takes over one function at a time, and only removes the old trunk once nothing depends on it anymore. You keep shipping the whole time. There is no big-bang cutover, no six-month freeze, no launch day where you pray the rewrite handles every edge case the old system quietly handled for a decade. This is how you actually retire legacy code that a business still runs on.
Why the rewrite fails almost every time
The full rewrite is the most seductive and most destructive move in software. It sounds clean: throw away the mess, build it right this time. In practice, the old system encodes years of hard-won edge cases nobody documented. Every weird conditional is a bug someone hit in production. Your rewrite does not know about them, so you reintroduce every one, live, in front of customers. Meanwhile the business asked you to freeze features for the duration, so you fall behind competitors while producing nothing shippable. Most rewrites either get cancelled halfway or ship late and broken. I would rather retrofit carefully than gamble the business, which is the same instinct behind rebuild vs retrofit for aging software.
How the strangler pattern actually works
You put a routing layer in front of the legacy system. Every request passes through it. On day one, the router sends everything to the old code, so nothing changes. Then you pick one capability, build a new implementation beside the old one, and flip the router to send just that capability to the new code. Everything else still goes to the legacy system. You verify, you watch, you move on to the next capability. Piece by piece, the router sends more traffic to new code and less to old, until one day nothing routes to the legacy system and you delete it.
The three moving parts:
- The interceptor. A facade, proxy, or router that owns all incoming traffic. This is the thing that lets you move work without callers noticing.
- The new implementation. Built one slice at a time, never all at once.
- The retirement step. Actually deleting the old path once traffic hits zero. Skip this and you get the worst outcome, two systems forever.
How to pick what to strangle first
This is where repo intelligence earns its place. You do not strangle in random order. You strangle by value and risk, and you get both from the data. Route the highest-churn, highest-pain modules out first, because those are the ones charging you the most every week. I lay out how to find them in Churn vs Complexity: Find Your Real Hotspots, and the same prioritization logic drives which tech debt to fix first.
Also strangle at the seams. Find the parts of the legacy system with the fewest connections to the rest, the modules with low coupling. Those are cheap to extract because cutting them does not unravel ten other things. A dependency map, which a tool like ReformCode generates from the actual code, shows you where the natural fracture lines are so you extract along them instead of hacking through the middle of a tightly wound knot.
The rules that keep it safe
Never let both paths handle the same request in production without a plan. Route cleanly. A given capability lives in old or new, not half in each, or you get impossible-to-debug inconsistencies.
Write characterization tests against the old behavior before you replace it. You are not trying to make the new code correct in the abstract. You are trying to make it match what the legacy system actually did, edge cases and all. Capture the old behavior as tests first, then build to pass them.
Keep the interceptor dumb. The moment your router grows business logic, it becomes a third system you have to maintain. It routes. That is all.
Delete as you go. The retirement step is not optional cleanup for later. Every legacy path you leave alive after its traffic hits zero is dead weight that confuses the next engineer and inflates your maintenance surface. Kill it the day it goes quiet.
Why this beats the alternatives for a real business
The strangler pattern keeps you shipping. You never freeze features, so the business never falls behind while you clean house. Each slice is small enough to reason about, test, and roll back, so risk stays bounded. And you get value continuously instead of at a distant, uncertain finish line. That is the same reason I favor incremental, reversible moves everywhere I operate: small steps you can undo beat big bets you cannot.
Legacy code is not a thing you defeat in one heroic quarter. It is a thing you dismantle deliberately, one strangled capability at a time, while the lights stay on. Start with the hotspots, cut at the seams, capture the old behavior in tests, and delete the trunk when nothing leans on it anymore.