Skip to content
← All insights

Replatforming without the rewrite that kills you

9 min readOptima

There is a conversation that happens in most companies running a system older than about eight years. The system works. It also takes three weeks to make changes that ought to take three days, nobody wants to touch the billing module, and the framework it is built on stopped receiving security patches some time ago.

Someone proposes rewriting it. Everyone agrees this is obviously correct. Eighteen months later the rewrite is 70% complete, the old system has continued to accrue changes the new one does not have, and the gap is widening faster than it is closing.

This outcome is common enough that it is worth being precise about why, because the reasons are structural rather than a matter of the team not being good enough.

Why big-bang rewrites fail

The target moves. The business does not stop for eighteen months. Every change made to the old system during the rewrite is a change the new one must also absorb, so you are chasing a system that is running away from you.

Value arrives only at the end. A rewrite that is 90% done delivers nothing. There is no partial credit, which means there is no point at which the project can be stopped and still have been worth doing. That is a bad risk profile for something measured in years.

The requirements are in the code. Most legacy systems encode a decade of undocumented business rules: the special discount for one customer class, the reconciliation quirk that exists because a regulator asked for it in 2014. Nobody remembers these. They are discovered when the new system gets them wrong in production.

Confidence peaks at the wrong time. Enthusiasm is highest at the start, when the least is known. By the time the genuinely hard parts surface, the budget is largely spent.

The alternative: strangle it

The strangler fig pattern, named for the plant that grows around a host tree until it can stand without it, replaces a system incrementally while it stays in production.

You put a routing layer in front of the existing application. Initially it forwards everything unchanged. Then, capability by capability, you build replacements behind it and move traffic across.

Strangler pattern routing over three stagesA routing layer sits in front of both systems. At the start it forwards all requests to the legacy application. As capabilities are migrated one at a time, an increasing share of routes is directed to new services, while the legacy application shrinks. Both run in production throughout, and any route can be moved back by changing a rule.requestsRouting layernginx, an API gateway, or IIS URL RewriteLegacy applicationshrinkingNew servicesgrowingMIGRATION, ONE CAPABILITY AT A TIMEstartall routes on legacymida normal state, sometimes for yearsendlegacy retired
Each migrated capability ships on its own and can be rolled back by changing a routing rule. The middle row is the important one: a half-migrated system is a normal operating state, not an unfinished project.

The routing layer can be nginx, an API gateway, or IIS URL Rewrite. The technology matters far less than the fact that something sits in front and can redirect a single route without redeploying either side.

Three properties make this work where a rewrite does not. Each migrated capability delivers value the moment it ships. Each one can be rolled back by changing a routing rule. And the project can be paused at any point, because a half-migrated system is an entirely normal state to operate in, sometimes for years.

Move by capability, not by layer

The most common mistake once teams adopt this pattern is migrating horizontally: replace the whole UI first, then the whole service layer, then the database.

This reintroduces the original problem. A new UI on an old backend delivers nothing until the backend follows, so you are back to deferred value and a long-running integration risk.

Migrate vertically instead. Take one capability, such as “customer registration” or “invoice generation”, and move its UI, logic and data together. It ships complete, works end to end, and proves the approach on something small before you commit to something large.

Choose the first candidate for boring reasons: high enough traffic that you learn something real, low enough risk that failure is survivable, and few enough dependencies that you are not migrating half the system by accident. Reporting and read-only views are often ideal first moves. Billing almost never is.

Data is the hard part

Routing HTTP is easy. Splitting data is where replatforming projects actually get stuck, because for a period both systems need a consistent view of the same information.

Three approaches, roughly in order of preference:

Shared database, split code. Both systems read and write the same tables while application logic migrates. Architecturally impure and completely fine as an interim state. It removes synchronisation from the problem entirely and lets you focus on behaviour. Most migrations should start here.

Change data capture. The new system maintains its own schema, kept current by streaming the old system’s transaction log. Reads move first, writes stay on the legacy side until the new model is proven. One-directional, which makes it far easier to reason about than the alternative.

Dual writes. Both systems written to on every change. This is the option that looks simplest and is not. You have created a distributed transaction without a coordinator, and the two stores will diverge the first time one write succeeds and the other fails. If you must, make one store authoritative, treat the other as derived, and build reconciliation from the start rather than after the first incident.

Whichever you choose, write the reconciliation job early. Something that compares both sides and reports differences will find problems weeks before your users do, and the confidence it provides is what lets you move faster later.

Do not port the bugs

There is a strong temptation to reimplement existing behaviour exactly, on the grounds that it is safer. It is safer, and it is often the right call, but not universally.

Legacy systems accumulate behaviour nobody chose: a rounding rule that came from a framework default, a validation that fires on the wrong field, a workflow step everyone works around. Migration is the one moment when questioning these is cheap.

The distinction worth drawing is between behaviour that is load-bearing and behaviour that is merely present. Load-bearing behaviour has downstream consumers: a report, an integration, a regulator, a customer’s own process. Preserve it exactly, even where it is ugly. Behaviour that is merely present can be fixed, and the migration is the cheapest opportunity you will get.

Telling them apart requires talking to the people who use the system, which is generally the most valuable and least performed activity in a replatforming project.

When a rewrite is actually right

Incremental migration is the default, not a law. There are cases where it is the wrong choice:

  • The system is genuinely small. Under a few thousand lines with a clear boundary, a rewrite may take weeks and carry little risk. The strangler pattern has overhead that is not worth paying at that size.
  • The domain model is fundamentally wrong. If the core abstraction no longer matches the business, such as a single-tenant model in a company that now sells to enterprises, incremental change can mean building around a mistake indefinitely.
  • The platform is a hard stop. A runtime that no longer receives security patches, or hardware you cannot buy, sets a date that overrides architectural preference.

Even then, the sequencing advice holds: find a way to deliver something to production in the first two months. A rewrite that reaches users early is a different, much better-behaved project from one that integrates at the end.

What to do on Monday

If you are looking at a system in this position, three things are worth doing before any architecture discussion:

  1. Put something in front of it. Even if it only forwards traffic, a routing layer converts “rewrite the system” into “move one route at a time”, and that reframing is most of the value.
  2. Instrument what is actually used. Legacy systems typically contain large amounts of code serving almost no traffic. Knowing which endpoints matter tells you what to migrate and, more usefully, what to delete.
  3. Pick one capability and move it. Not a design document, but a shipped, live, vertical slice. You will learn more about the real difficulty in six weeks of doing it than in six months of planning it.

The goal is not a modern system. It is a system that can absorb the next change without another programme like this one.

Working on something similar?

Tell us what you are building. We will come back with an honest view of scope, approach and timeline.

We reply within one business day.