Skip to content
Antegrate

Technical note

Why rewriting a legacy system is often the wrong first move

A rewrite proposal is a reasonable reaction to a system that has become expensive to change. It is also a diagnosis-free one. Before a business commits to replacing software it runs on, it is worth being precise about what the rewrite is meant to fix — and whether replacing the code fixes it.

Written by
Dorian Ben Haim, Principal
Published
Reading time
9 minutes

The proposal

A rewrite proposal is a measurement, not a diagnosis

Rewrites get proposed at a familiar moment. The system still runs the business, but a change that sounds small comes back estimated in quarters. Releases are events people plan their weekends around. The engineers who know where everything lives are tired of being the only ones who do. At that point a clean start stops looking like an indulgence and starts looking like the only way out.

The frustration is real, and it is information. It says the cost of change has become visible to the business. What it does not say is where that cost comes from. A rewrite is a bet that the cost belongs to the code as a whole — rather than to a few specific places in it, or to the way the code is built, tested and released.

That bet is sometimes right. It is wrong often enough, and expensively enough, that it should be the conclusion of an investigation rather than the starting point of one.

What the system knows

The valuable part of an old system is not its code

A system that has run a business for years is mostly a record of decisions. Every odd branch in the pricing logic, every special case for one customer, every retry that waits a particular number of seconds exists because something happened once and somebody made the system handle it. Almost none of it is written down anywhere except in the code.

That is what a rewrite actually has to reproduce, and it is the part nobody can list. Requirements for the new system are gathered in workshops, from people who remember the rules they meet every week and forget the ones that fire twice a year. The rest is rediscovered in production: each defect in the new system turns out to be a requirement the old one was quietly meeting.

The old system is the only complete specification of itself. A rewrite discards the specification and keeps people’s memory of it.

The same is true at the edges. Other systems depend on what the current system does, not on what it was meant to do — the field that is always populated although the documentation calls it optional, the export that happens to arrive sorted, the error message another team has learned to parse. Hyrum’s Law states it precisely: with enough consumers, every observable behaviour of a system will be depended on by somebody. A rewrite changes observable behaviour by definition, so every integration becomes a migration project of its own.

The cost nobody budgets

For as long as the rewrite lasts, you own two systems

The old system does not stop needing changes because its replacement is being built. Customers, partners and regulators do not wait. Every change in that period is either made twice — once in the system that runs the business, once in the one that is meant to — or deferred until the new system arrives, which freezes the business for as long as the rewrite takes.

Neither cost appears in the rewrite estimate. The estimate prices a new system against a snapshot of the old one. The old one keeps moving, and the target moves with it.

Then there is parity. A replacement is not judged against its own plan; it is judged against everything the old system does, on the day someone proposes switching over. The last stretch of parity — the report finance runs once a quarter, the admin screen one person depends on, the batch job nobody remembers scheduling — is where the schedule goes, because it is exactly the part that was never written down.

Fred Brooks named the other risk in 1975: an architect’s second system is the most dangerous one they design, because it tends to carry every idea that was held back the first time. A rewrite is a second system with a deadline, built while the business still depends on the first.

What stays the same

Most of what makes a system hard to change survives a rewrite

It helps to be specific about where change cost comes from. In a system that has become hard to change, the causes tend to come from a short list:

Missing boundaries
A change touches many places because nothing isolates one concern from another.
Fear of release
Deployment is manual, infrequent and understood by one person, so every change inherits the risk of the release that ships it.
Silent failure
Integrations break without announcing it, so every change has to be verified by hand — and by luck.
Concentrated knowledge
The few people who can change the system safely become the queue every change waits in.
No executable description of behaviour
Without tests that pin down what the system does, every change is a guess about what else it affects.

Read that list again with a rewrite in mind. Only the first item is about the structure of the code, and even that one returns if the conditions that produced it are unchanged. The rest are properties of how a system is built, released and operated. A new codebase written by the same organization, under the pressure of reaching parity with a system the business still depends on, inherits all of them. It becomes the next legacy system — and faster than the first one did, because it was built in a hurry.

Moving a bad architecture to the cloud gives you a bad architecture in the cloud. Rewriting a system without changing how it is built gives you a newer system with the same problems.

The first moves

What to do before deciding whether to rewrite

None of this means a hard-to-change system should be left alone. It means the first moves should be the ones that pay off whatever is decided later: work that reduces the cost of change now, and makes the rewrite question answerable with evidence rather than frustration.

Measure where change actually costs
Take the recent changes that took far longer than expected and trace where the time went. Change cost is rarely spread evenly across a codebase. It concentrates — in a duplicated decision, a missing boundary, a release process — and most of the code is irrelevant to the problem. If the cost is concentrated, a whole-system rewrite spends most of its budget on parts that were never the problem.
Make failure visible
Before changing how the system works, make it announce when it does not: structured logs, correlation across the request path, alerts on the integrations that fail quietly. It is the cheapest high-value work available, and every later change — including a rewrite — depends on it.
Make releases boring
A scripted, repeatable deployment with a rollback measured in minutes removes the risk premium that currently sits on every change. Part of every estimate is the cost, and the fear, of the release that ships it. Take that away and the estimates change.
Pin down the behaviour
Characterization tests — Michael Feathers’ term for tests that record what a system does today, rather than what it should do — turn undocumented behaviour into something a change can be checked against. They are also the only practical specification a replacement would ever have.
Put a boundary around the expensive part
Once the costly area is known, isolate it behind an interface that the rest of the system talks to. From then on it can be changed, or replaced, without the rest of the system noticing.
Replace incrementally, behind that boundary
Martin Fowler’s strangler fig pattern describes the approach: build the new implementation around the edges of the old one, move behaviour across a piece at a time, and let the old system shrink until it can be switched off. Every step ships, every step can be reversed, and the business never waits for a cutover weekend.

This is not the opposite of a rewrite. If the whole system does turn out to need replacing, this is how to replace it safely — one bounded piece at a time, with the old system still running and still correct. The difference is that each step is justified by evidence about where change costs, and the work can stop as soon as the cost of change is acceptable, rather than when the new system finally reaches parity.

The exceptions

When a rewrite is the right answer

Restraint is a position, not a rule. There are situations where replacement is correct:

The platform is at a dead end
The runtime, framework or vendor is out of support, security fixes have stopped, and there is no incremental migration path. Even then, it is worth checking whether replacement can proceed a piece at a time behind a boundary rather than all at once.
The behaviour is no longer the asset
The business has changed enough that what the old system encodes is no longer what the business does. If the rules are obsolete, there is little to preserve and little to rediscover.
The system is small and fully understood
When the behaviour can be specified completely — ideally because it is already captured in tests — the rediscovery risk is small, and so is the case against starting again.
Nothing can be isolated
If there is no seam anywhere — no place to put a boundary, no piece that can be replaced without replacing everything — incremental replacement is not available, and the choice really is between living with the system and replacing it.

What these have in common is that each is a conclusion reached by examining the system. That is the test for any rewrite decision: it should be the output of a diagnosis, not a substitute for one.

Before committing

Questions to answer before committing to a rewrite

  • On the last few changes that took far too long, where did the time actually go — into the code, or into release, verification and coordination?
  • Which behaviours of the current system can nobody specify without reading the code?
  • Which systems, reports and people depend on what the current system does today — including what it does by accident?
  • What will the old system need while the new one is being built, and who will do that work?
  • What will be different about how the new system is built, tested and released — and why can that not be applied to the current system first?
  • What does parity mean, specifically, and who decides it has been reached?
  • What is the smallest piece that could be replaced first, and what would replacing it prove?

If these have good answers, the rewrite may well be the right call, and the answers become its plan. If they do not, the rewrite is not a plan yet. It is a hope that the new system will not have the problems of the old one.

Written by

Dorian Ben Haim leads Antegrate, a principal-led software engineering consultancy for business-critical systems that have become hard to change. About the practice

Answering the rewrite question is what the assessment is for.

The Technical Systems Assessment is a three-week, fixed-fee diagnosis of why a system has become expensive to change. Its recommendation says what should change and, explicitly, what should not be rewritten.

A credible assessment may conclude that a rewrite is right. It may also conclude that a rewrite is the most expensive way to leave the problem where it is.