Modernization
Legacy modernization: the strangler fig in practice
Big-bang rewrites fail for reasons that are well understood. Incremental replacement works — if you have the seams, the tests, and the discipline.
Every few years an organization decides its legacy system is beyond saving and commissions a from-scratch rewrite. Most of these projects run over budget, ship late, and deliver a system that reproduces the old one's quirks without its hard-won reliability. The failure is not a lack of talent. It is a category error: treating a living system as if it could be paused and replaced wholesale.
Why big-bang rewrites fail
A legacy system encodes years of edge cases, regulatory patches, and bug fixes that nobody remembers the reason for but that customers depend on. A rewrite starts by throwing all of that undocumented knowledge away and rediscovering it, in production, one incident at a time. Meanwhile the old system keeps evolving to meet the business, so the rewrite is chasing a moving target it never catches.
“You are not rewriting the code you can see. You are rewriting the ten years of decisions you cannot see, and the system remembers them even when your team does not.”
The strangler fig pattern
The alternative, named after a vine that grows around a tree until it can stand on its own, is to wrap the legacy system in a routing layer and replace it one capability at a time. New functionality is built in the new architecture; existing functionality is migrated piece by piece; the router sends each request to whichever system currently owns that capability. The old system shrinks until it can be removed.
// Route each capability to old or new as it migrates
const routes = {
"/invoices": "modern", // migrated
"/payments": "modern", // migrated
"/reports": "legacy", // not yet
} as const
export function route(path: string) {
const owner = matchPrefix(path, routes) ?? "legacy"
return owner === "modern" ? modernService(path) : legacyService(path)
}The prerequisites nobody wants to hear about
The pattern only works if you can carve the system at its seams, and legacy systems are often a single tangled unit. Before any migration, you need characterization tests that capture current behavior — including the bugs — so you can prove the new implementation matches. You also need observability across both systems so the routing layer's decisions are visible and reversible.
- Characterization tests that pin down actual behavior before you change anything.
- A routing seam (API gateway, proxy, or facade) you fully control and can flip per capability.
- Feature flags so a migrated capability can be rolled back in seconds, not a deploy cycle.
- Shared observability so you can compare old and new behavior on live traffic.
Sequencing the migration
Start with a capability that is high-value and low-risk — something that delivers a visible win and builds organizational confidence without threatening critical revenue. Save the riskiest, most entangled core for last, when the team has the most experience with both systems and the surrounding capabilities have already been extracted, reducing its blast radius.
The strangler fig is slower to start and far safer to finish. At every step you have a working system, a reversible change, and a smaller legacy footprint than the day before. That is the opposite of a big-bang rewrite, where you have nothing shippable until the very end — the moment you understand the old system's hidden contracts least and need to understand them most.
Working through a challenge like this? Clifftech embeds senior engineers and AI specialists who have shipped it before.
Start a conversation