The instinct when a codebase has slowed to a crawl is to rewrite it. The harder, more reliable answer is to carve it apart one module at a time while the product keeps shipping.

The rewrite is the wrong default

A full rewrite is easy to sell internally. It has a clean story: stop adding to the mess, build the right thing from scratch, cut over on a fixed date. Boards like it because it sounds decisive. Engineers like it because greenfield code is more fun to write than legacy code is to read.

It also has a track record, and the track record is bad. The business logic buried in a five-year-old billing module — the tax exception for one enterprise customer, the proration rule from a pricing change nobody remembers approving, the retry behaviour that exists because a payment provider once had a bad week — does not show up in a spec. It shows up in production, usually as a bug report, usually after the old system has already been switched off. Teams that commit to a rewrite tend to discover this logic in the worst possible order: late, expensive, and irreversible. A b2b saas legacy modernisation effort built on “we’ll rebuild it properly” is a bet that the team can reconstruct years of undocumented decisions faster than they were made. That bet rarely pays off inside the timeline anyone approved.

The strangler-fig approach

The alternative is to leave the monolith running and grow a replacement around it, one bounded module at a time. The name comes from the strangler fig tree, which germinates in the canopy of a host tree and sends roots down around it — the host keeps functioning until, eventually, it is no longer structurally necessary.

Applied to software: pick one module with a defined boundary — billing, notifications, entitlements — and put a stable interface in front of it. Everything that used to call into the module directly now calls the interface instead. Behind that interface, you can rebuild the module’s internals without touching anything else, because nothing else knows the internals changed. The rest of the system keeps shipping features throughout, because the rest of the system is untouched.

The discipline is in choosing the boundary. A module extracted along the wrong seam — one that still shares a database table or a hidden side effect with three other parts of the system — just moves the coupling instead of removing it.

A billing module, one quarter, in the open

One extraction we ran looked like this. The billing module sat inside the monolith, wired directly into the order flow, the admin panel and a nightly reconciliation job — three callers, one shared table, no interface between them.

Weeks 1–3: define the interface the rest of the system should have had all along — issue invoice, apply credit, close period — and route all three callers through it, still hitting the old logic underneath. No behaviour changed. This step alone surfaced two undocumented proration rules that only the admin panel exercised.

Weeks 4–9: build the new billing service behind that interface, and run it in shadow — every real invoice generated by the old logic and, silently, by the new logic in parallel, with the two compared and any mismatch logged rather than surfaced to a customer. This was the ugly middle: two billing systems existing at once, on-call carrying both, a growing list of intentional and unintentional discrepancies to triage, and no user-visible progress to show for six weeks of work. It is the point at which projects like this get quietly abandoned, because the finished result is still invisible and the interim cost — running two systems — is entirely visible.

Weeks 10–13: once the shadow comparison held clean for a full billing cycle, cut traffic over module by caller — reconciliation first, since it was lowest-risk, admin panel last, since it carried the most edge cases — and delete the old code once each caller was confirmed live on the new path. The time to close a billing period dropped substantially; more to the point, nothing broke for a customer during the transition, because at every point exactly one caller was on the new logic and everything else was unchanged.

The honest trade-off

Incremental modernisation is slower to show a finished result. There is no day-one demo, no “we rebuilt it” announcement — just a series of boundaries that get quietly redrawn while the product keeps moving. That makes it harder to sponsor internally than a rewrite, and it requires someone senior enough to hold the line when six weeks in, the shadow-run phase looks like effort with nothing to show.

A rewrite is faster to imagine and slower to deliver, because the estimate is made before the hidden logic is found, and the strangler fig is slower to imagine and faster to deliver, because the hidden logic surfaces early, one module at a time, while it’s still cheap to fix.

Treat it as an operating model, not a project

The teams that do this well don’t run one extraction and stop. They build an engineering velocity operating model around it — a standing practice of identifying the next module whose age is costing the most in delivery speed, extracting it behind an interface, and moving to the next one, quarter after quarter. Modernisation stops being a project with an end date and becomes a routine cost of keeping the system fast, which is a much easier thing to keep funded than a rewrite ever was.