· 4 min read

Modernizing a legacy system without a big-bang rewrite

Full rewrites stall the roadmap and often fail. The strangler fig pattern replaces a legacy system piece by piece while it keeps serving users.

Software developmentArchitectureModernization

Every growing company eventually has one: the system that makes the money, that nobody fully understands, and that slows down every change. The tempting answer is a rewrite — freeze the old system, build the new one properly, switch over. It rarely goes to plan. The old system keeps changing because the business does, the new one has to catch up with years of edge cases nobody wrote down, and the switch-over date keeps moving while the roadmap stands still.

The alternative is to replace the system incrementally, while it keeps running. Martin Fowler named this the strangler fig pattern, after the vine that grows around a tree until it can stand on its own. Each step is small, ships to production, and can be rolled back.

Start with a seam

Incremental replacement needs a place where you can divert traffic from old to new. For a web application or API, that is usually a routing layer in front of the legacy system — a reverse proxy, an API gateway or an ingress — that can send a given path, customer or percentage of traffic to the new implementation. Put the proxy in place first, routing everything to the old system. It changes nothing for users and gives you the control you need for every step after.

Inside the codebase, the equivalent is branch by abstraction: introduce an interface in front of the piece you want to replace, move callers onto the interface, then swap the implementation behind it — with a feature flag, so you can switch back.

Pin down behaviour before you move it

Legacy systems are full of behaviour that is load-bearing but undocumented. Before replacing a piece, write characterization tests: tests that record what the system does today, including the odd parts, rather than what the specification says it should do. Recorded production requests and responses make good test fixtures. When the new implementation disagrees with them, you decide case by case whether that is a bug fix or a regression — instead of finding out from a customer.

Pick the first slice carefully

  • Choose something with clear boundaries and few dependencies on the rest of the system.
  • Prefer an area that changes often, so the new code pays off quickly in faster delivery.
  • Avoid the scariest, most central part first; build confidence and tooling on something smaller.
  • Make it small enough to reach production in weeks, not quarters.

Read-heavy features are often a good start: a new service can serve them from a replica or a synced copy of the data without taking over writes yet.

Data is the hard part

Moving code is comparatively easy; moving data ownership is where modernization projects get stuck. Avoid having two systems write to the same tables for long. Common approaches, roughly in order of effort:

  • Let the new service read the legacy database directly at first, while the old system keeps ownership of writes.
  • Stream changes out of the legacy database with change data capture — Debezium is a common choice — into the new service's own store.
  • Move ownership of a set of data to the new service, and have the legacy system call it through an API instead of the table.

Whichever you choose, keep schema changes backward compatible while both systems run. Expand first — add the new column or table — migrate, and only contract once nothing reads the old shape.

Prove it in production before switching

Before the new path takes real traffic, run it in parallel. Send a copy of production requests to the new implementation, discard its responses, and compare them with the legacy output. The differences are your to-do list. Then shift real traffic gradually — internal users first, then a small percentage, then everyone — with a fast way back. This is the same discipline as a canary deployment, applied to an architecture change.

Finish the job

The most common failure of incremental modernization is stopping halfway: two systems, both in production, both maintained, forever. Track what is left in the legacy system as explicitly as what has been built, and delete old code paths as soon as traffic has moved off them. Deleting is how you collect the savings.

When a rewrite is the right call

Sometimes it is. If the system is small, the platform it runs on is end-of-life with no migration path, or the product itself is changing so much that the old behaviour does not need preserving, a rewrite can be cheaper. The test is honest: can you describe everything the current system does? If not, the incremental route is usually the safer one.

Incremental modernization is a large part of our software development work, and it is how a payments monolith moved to Kubernetes with no cutover downtime. If a legacy system is holding your roadmap back, get in touch.

Let's build something awesome.

Tell us about your infrastructure or product challenge. We'll reply within one business day.