Skip to content
Antegrate

WritingLegacy modernization

How to modernize a legacy system without rewriting it

Incremental modernization is not a compromise between leaving a system alone and replacing it. It is a sequence: make the system observable and safe to release, pin down what it does, then move the expensive parts behind boundaries and replace them one at a time — with the system in production throughout.

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

In short

  • Modernize in the order that change costs, not the order that code looks old: find where changes are expensive, and start there.
  • Make the system safe to change before changing it — observability, automated releases with fast rollback, and characterization tests that pin down current behaviour.
  • Then create seams around the expensive parts and replace what sits behind them one piece at a time, using patterns such as strangler fig, branch by abstraction and an anti-corruption layer.
  • Every step ships to production and can be reversed. The work is finished when the cost of change is acceptable, not when every line is new.
  • Move data last, and move ownership of it before moving its storage.

The principle

Modernize where change costs, in the order it costs

A modernization programme that starts with the oldest code or the least fashionable framework spends its budget on what looks worst rather than on what costs most. The expense of change is rarely spread evenly. It concentrates in a few places — a duplicated decision, a missing boundary, a release process — and most of the codebase is irrelevant to the problem.

So the first task is to find those places. Trace a handful of recent changes that took far longer than expected and note where the time went. Look at version-control history for the components that change most often, and most often together. Then weigh frequency against difficulty: a tangled module nobody touches is not a priority, while a moderately messy one that every feature passes through probably is.

Code nobody needs to modify is not costing anything.

The result is a short list of hotspots, ranked by what they cost the business. That list, not the age of the stack, sets the order of the work — and makes clear which parts of the system can be left exactly as they are. Seven signs your software has become too expensive to change covers how to measure each symptom.

Step one

Make the system safe to change

Before changing how the system works, make changes safe. Three things do most of the work:

Observability
Structured logs, a correlation ID that follows each request across components, and dashboards and alerts on business outcomes — orders processed, messages delivered — rather than only on CPU and memory. Without it, there is no way to tell whether a change made things better or worse.
Automated, repeatable releases
A scripted deployment, versioned database migrations, automated verification after each release, and a rollback that takes minutes. This lets changes ship in small batches, and a small batch is a small risk.
Characterization tests
Michael Feathers’ term for tests that record what the system does today, rather than what it should do. For logic whose output matters — prices, invoices, exports — capture real outputs and assert that refactoring leaves them unchanged. These tests are what make it safe to touch code nobody fully understands.

None of this changes what the system does, and that is the point. It makes every later change cheaper and safer, whatever that change turns out to be — including a rewrite, if one is eventually justified.

Step two

Create seams around the expensive parts

A seam, in Michael Feathers’ phrase, is a place where behaviour can be changed without editing the code around it. In practice it is an interface: the rest of the system calls it instead of reaching into the implementation, so what sits behind it can be tested, changed or replaced on its own.

Three patterns cover most situations:

Strangler fig — replacing from the edges
Put a routing layer in front of the old system and move behaviour to a new implementation one route or feature at a time, until the old system can be switched off. Described by Martin Fowler and documented as an architecture pattern by Microsoft.
Branch by abstraction — replacing from the inside
For a component buried deep in the system: introduce an abstraction over it, move its callers onto the abstraction, build the new implementation behind it, switch, and delete the old one. The technique lets the replacement happen on the main line of development, without a long-lived branch.
Anti-corruption layer — keeping old models out
A translation layer between the legacy system — or a partner’s system — and new code, so that the old model’s names, quirks and assumptions do not leak into what replaces it. Without one, the new code inherits the coupling it was meant to remove. See the pattern description.

The boundary is the structural core of the work. Once it exists, the expensive part stops leaking cost into everything around it — even before anything behind it is replaced.

Step three

Replace one piece at a time, in production

Choose the first piece for what it proves, not for how bad it is: a part with real change cost, reasonably isolated, and without the heaviest data dependencies. It establishes the pattern, pays for the first boundary, and shows whether the estimates hold.

Where the new implementation must behave exactly like the old one, run both and compare. Send the same inputs to each, compare the outputs — prices, totals, documents — and switch only when every difference is understood. Put the switch behind a flag, so it can be reversed in minutes.

Interfaces and database schemas change the same way, by expand and contract: add the new form alongside the old, move every caller across, then remove the old form. At no point does anything have to change in a single coordinated release.

Then delete the old path. Deletion is part of done.

Data

Move data last, and ownership before storage

Data is where incremental migrations stall. Two implementations that both write the same records drift apart, and dual writes without a clear owner become a permanent reconciliation problem.

The order that works is to give each piece of data a single owner first — one component that writes it, with everything else reading through an interface. Then move storage behind that owner: backfill the new store, keep it in sync with change data capture or an incremental job, verify with reconciliation reports, switch reads, and finally switch writes. Each stage can be checked and reversed, and none of them requires a migration weekend.

Progress

How to know it is working

Measure the same things that identified the problem: how long a change in the affected area takes from request to production, how often releases there fail, how many people a change there requires, and whether estimates for comparable work have come down. If those numbers do not move, the work is going to the wrong place — and it is better to learn that after one step than after a year.

A modernization is finished when changing the system costs what the business is willing to pay — not when the last line of old code is gone. Some parts may never need replacing, and a programme that knows that is cheaper than one that does not.

Pitfalls

Where incremental modernization goes wrong

Modernizing the parts that look worst
Old, ugly code that rarely changes is not where the money goes. Rewriting it first produces visible progress and no change the business can feel.
Starting with the hardest piece
The first replacement should prove the approach and pay for the boundary. Choosing the riskiest component first turns an incremental programme into a big bet with extra steps.
Never deleting the old path
Two implementations of the same behaviour cost more than one. Removing the old path is part of every step, not a clean-up phase that never arrives.
Seams in name only
An interface that exposes the legacy system’s tables or data structures carries the coupling straight through. A seam only helps if what sits behind it can change without anything in front of it noticing.
Freezing features to modernize
Modernization that stops the business from changing has the same problem as a rewrite. The economical route is to modernize the parts a committed business change passes through, so the change pays for the boundary.

The alternative — replacing everything at once — carries its own costs, covered in why rewriting a legacy system is often the wrong first move.

Questions

Questions and answers

How long does it take to modernize a legacy system incrementally?

It depends on where the change cost sits, not on the size of the codebase. Because each step ships on its own, the first improvements — observability and automated releases — can land within weeks, and the work can stop whenever the cost of change is acceptable.

What is the difference between refactoring and modernization?

Refactoring changes the structure of code without changing its behaviour. Modernization is broader: it can include refactoring, but also release automation, infrastructure, integration boundaries and replacing components, all aimed at reducing the cost of change.

What is a seam in legacy code?

Michael Feathers’ term for a place where behaviour can be changed without editing the code around it. In practice it is an interface the rest of the system calls, behind which an implementation can be tested, changed or replaced.

Should we move a legacy system to the cloud as part of modernization?

Only if it reduces the cost of change or a real operational risk. Moving a system with an architecture problem to the cloud relocates the problem and adds a metered bill to it. Infrastructure work is worth doing when it makes changes faster and safer.

Can we keep shipping features during modernization?

Yes, and the business should. The most economical modernization rides along with changes the business already needs: the committed feature pays for the boundary it passes through, and every later change reuses it.

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

Find out which parts are worth modernizing first.

The Technical Systems Assessment establishes where change cost accumulates in a system and why, and turns that into a sequenced, costed roadmap — including what should be left alone.

Three weeks, fixed scope, and specific enough for any competent team to execute.