Engineering · Jul 2026 · 14 min read
The strangler pattern, in practice — with a rollback at every step
Big-bang rewrites fail quietly for months before they fail loudly. Here's the seam-by-seam sequence we use to retire a monolith without ever stopping the business.
Nobody sets out to write a monolith. It accretes — a shared table here, a helper that grew teeth there — until the day a three-line change takes three weeks and the team stops volunteering for the hard parts. The instinct is to rewrite. The instinct is wrong.
A rewrite asks you to bet the business on a future system that doesn't exist yet, while the old one keeps changing under you. The strangler pattern refuses that bet. You carve one capability out at a time, run old and new side by side, and cut over only when the new path is proven — with a rollback rope in your hand the entire way.
Draw the seams before you write a line
The whole game is where you cut. A good seam follows a business capability with a clean data boundary; a bad seam follows the org chart or the folder structure. We start by mapping the domain, not the code, and we look for the capability that is painful, bounded, and low-risk to run in parallel. That's your first slice.
If you can't run the old and the new at the same time, you don't have a migration — you have a flag day. Flag days are how careers end.
The branch-by-branch cutover
With the seam drawn, you put a routing layer in front of the capability. Today it sends 100% of traffic to the legacy code. You build the new service behind the same router, replay production traffic against it in shadow mode, and diff the outputs until the differences are explainable. Only then do you move real traffic — 1%, 10%, 50% — watching the error budget the whole time.
export async function route(req: Request) {
const slice = req.headers.get('x-capability');
const useNew = await rollout.flag('checkout-v2', { slice });
const target = useNew ? checkoutV2 : legacy;
return target.handle(req); // same contract, two implementations
}Notice there is no 'and then we delete the old code' step in the plan. Deletion is a separate, calm, low-stakes change you make weeks later, once the old path has carried zero traffic long enough to be sure. Treating deletion as its own commit is the single biggest reducer of migration anxiety we know.
The data trap, and how to escape it
Code is the easy half. The trap is data: two systems, one source of truth, and a synchronization story that quietly rots. We pick one of three patterns per slice and write it down before we build:
- Shared database, separate schemas — fastest to start, hardest to leave; only for the first slice.
- Change-data-capture into the new store — the new service reads its own copy; writes still go to the old until cutover.
- Dual-write with reconciliation — most control, most ceremony; reserve it for the money tables.
Whichever you choose, build the reconciliation job on day one, not as a 'phase two' nicety. The job that compares old and new and pages a human on drift is the thing that lets you sleep during the 10% phase. Without it, you're not migrating — you're hoping.
Know when to stop strangling
The pattern is a means, not a religion. There comes a point where the remaining monolith is small, stable, and not worth another seam — and the cost of the routing layer starts to exceed the cost of the legacy code it's protecting. When the last painful capability is gone, stop. Leave the quiet old core alone, document the boundary, and spend the saved energy on the next thing that actually moves a metric. That restraint — knowing when a migration is finished — is the mark of a team that ships.
Put this to work on your stack
Every article here came out of a real engagement. If the problem sounds like yours, a free audit is the fastest way to see what it'd look like applied to your systems.
Get a free audit