Legacy app modernization without a rewrite: the strangler fig approach

Yellow fibre optic cables connected to a numbered patch panel

In July 2025 the US Government Accountability Office reported that the federal government spends more than $100 billion a year on IT, with about 80% of it going to operating and maintaining existing systems. GAO looked at 11 of the most critical legacy systems across 10 agencies. Only 3 had complete modernization plans.

Private companies rarely publish numbers like these, but the pattern is familiar. Most of the budget keeps old software running, so little is left to replace it. When the replacement finally gets funded, the obvious plan is to rebuild everything at once.

Why full rewrites of critical systems go wrong

A rewrite looks simple on paper: the old system already defines the requirements, so build the same thing on modern technology. In practice nobody fully knows what the old system does, the business cannot pause new features for years, and much of the existing behaviour is no longer needed. The plan underestimates all three.

Martin Fowler, who named the strangler fig approach, describes this plan bluntly in his article on the pattern: “we’ve seen this simple-sounding plan go down in flames most of the time.”

Hidden behaviour is the part that hurts most. A legacy system holds years of small decisions: a rounding rule added after an audit, a special case for one large customer, a report someone in finance relies on every month. None of it is in the documentation. A rewrite finds these one at a time, usually after launch.

How the strangler fig approach works

Put a routing layer in front of the old system so every request passes through a point you control. Then build replacements one capability at a time, send that capability’s traffic to the new code, and remove the old code once nothing uses it. The old system shrinks as the new one grows, and the business keeps running throughout.

The name comes from strangler figs, which grow around a host tree until they can stand on their own. Fowler took it from a trip to the Queensland rainforest.

The routing layer can be an API gateway, a reverse proxy, or a thin service that sits between the user interface and the back end. At the start it passes everything straight through to the legacy system. Each step of the modernization changes one route.

Fowler points to four activities defined by Ian Cartwright, Rob Horn and James Lewis: understand the outcomes you want, decide how to break the problem into parts, deliver the parts, and change the organization so the work can continue. The rest of this article covers how those play out on a typical business application.

Choose the first slice by change frequency and risk

Start with a part of the system the business changes often but that would not stop operations if something went wrong. Frequent change means the new version pays off quickly. Limited risk means the team can learn the process before touching the core.

Version control history and the ticket backlog show where change concentrates. Look for files and modules that appear in most recent work, and features that keep collecting requests.

Good first slices are often reporting, a read-only customer view, notifications or a new feature built outside the old code from day one. Poor first slices are the billing engine, the general ledger or anything that writes to the most shared tables. Leave those until the team has done a few migrations and trusts its tooling.

Find the seams

A seam is a place where you can intercept behaviour without editing the code on both sides: an HTTP route, a message queue, a scheduled file transfer, a database view. Fowler notes that well-designed systems would already have them, and that such systems are rare. Part of the early work is creating seams where none exist.

Expect to write code you will later delete: adapters that translate between old and new data formats, sync jobs that keep two stores consistent, feature flags that switch traffic. Fowler calls this transitional architecture. It can look wasteful, and he argues the lower risk and earlier value outweigh its cost.

Budget for it up front so nobody is surprised when a third of an early phase goes into scaffolding.

Run old and new side by side

Before switching a slice over, send the same input to both systems and compare the results. Differences show you behaviour the new code missed or behaviour the old code got wrong. Switch traffic gradually behind a flag, and keep a tested way to send it back.

Comparison runs catch the hidden rules mentioned earlier. A mismatch report after a week of real traffic is worth more than any requirements document written from memory.

Decide in advance what counts as a match. Timestamps, ordering and floating point rounding will differ in harmless ways, and the comparison needs to ignore them so real differences stand out.

Plan the data migration separately

Data is usually harder to move than code. Many legacy applications share one large database, and several features read and write the same tables. Moving a feature is straightforward. Moving the data it owns, while other features still depend on it, takes planning.

A common sequence has three stages. First, the new service reads from the old database through views. Next, changes are streamed or synced into a store the new service owns. Finally, ownership switches and the legacy code starts reading from the new source instead.

Avoid having application code write to both databases directly. When one write succeeds and the other fails, the two stores drift apart in ways that are hard to detect.

Retire the old parts on schedule

A modernization only reduces cost when old code, infrastructure and licences are actually switched off. Track retirement as a deliverable in every phase. Otherwise you end up running two systems and paying for both.

When a route has served the new system without problems for an agreed period, remove the legacy code behind it and shut down whatever it ran on. Update the runbooks and on-call notes at the same time.

This is also where the fourth activity, changing the organization, matters most. If teams keep the same release process, approval chains and ownership boundaries that produced the old system, the new one will drift toward the same state.

When a full rewrite is the right call

Incremental replacement suits large systems that the business depends on every day. A rewrite can be the better option when the system is small, well understood and loosely connected to everything else, or when the platform underneath it has no remaining path forward.

Even then, keep the parts of this approach that lower risk. A comparison run, a gradual traffic switch and a rollback plan cost little next to a failed launch.

Our legacy app modernization work follows this phased approach: assess the system, plan the order, modernize component by component and validate each step against real workflows. If you have a system the business depends on and cannot afford to stop, talk to us about it.

Sources