The full rewrite is attractive because it promises a clean break, and that is also why it is dangerous. The old system holds years of decisions nobody wrote down. It keeps changing while you rebuild it. And the business gets nothing until the end, which is when you discover the requirements you missed. We advise against the big-bang approach by default and treat it as an exception that has to be argued for.

Find out what the system actually does

Documentation of a legacy system describes what someone once intended. The running code is the specification, bugs included, and some of those bugs are now features that a customer or a downstream report depends on.

Michael Feathers, in Working Effectively with Legacy Code, describes characterisation tests: tests that record what the code currently does, as opposed to what it ought to do. You call the code with a realistic input, see what comes out, and assert that result. When a test later fails, you know behaviour has changed, and you can decide whether that was intended.

  • Start at the highest level you can get at, such as an HTTP endpoint, a batch job's input and output files, or a generated report.
  • Use anonymised samples of real inputs where you can, since real data contains the odd cases.
  • When you find a bug, don't fix it in the same change. Record it, and decide separately.
  • Target the code you are about to change rather than chasing overall coverage.

Route around it with the strangler fig

The strangler fig pattern, named by Martin Fowler, replaces a system gradually. New functionality grows around the old system, taking over its responsibilities piece by piece until the old system can be switched off.

The essential mechanism is a seam where you can redirect work: a reverse proxy, an API gateway, a message router or a facade in the code. Once that exists, the migration becomes a sequence of small routing decisions, each of which can be reversed.

  1. Put the routing layer in front of the old system, with all traffic still going to the old code. Ship that alone.
  2. Pick one slice. Good candidates change often or block something the business wants, and have clear boundaries.
  3. Build the replacement and compare it. Send it a copy of real traffic and compare responses with the old system's, without returning its answers to users.
  4. Move a small share of traffic, watch it, then increase it. Keep the route back to the old code until you are confident.
  5. Delete the old code path. This step gets skipped constantly, and skipping it means you now run two systems.

Your first slice should be real but survivable. A read-only report or lookup is often a good place to prove the routing, monitoring and deployment before you take on anything that writes data.

Data is the hard part

Moving code is rarely what stalls these projects. Shared databases are. If old and new systems both write to the same tables, the old schema constrains the new design and each system can break the other's assumptions.

Give each piece of data one system of record at any moment, and make the handover explicit. Change data capture can keep a read copy in the new system while the old one remains the writer. Avoid application-level dual writes where you can, because when one write fails and the other succeeds, you have a reconciliation problem. Whichever route you take, run a regular reconciliation that compares the two stores and reports differences.

When a rewrite is fair

Sometimes it is the right call. The system may be small enough to replace in weeks, or the platform may be going out of support with no route for incremental change. Even then, keep the characterisation tests and plan a parallel run, so you have a way to show the replacement behaves as it should before you cut over.

Set conditions for stopping

Incremental work can drift. Agree in advance what would finish the programme, and be willing to stop once the parts that were hurting are gone. A stable, rarely changed module can stay where it is, provided it is supported and secure. The goal is a system you can change safely, and old code that nobody needs to touch doesn't stand in the way of that.

What to do this week

  1. Find your change hotspots. Run git log --since="1 year ago" --name-only --format="" | sort | uniq -c | sort -rn | head -20 to see which files change most.
  2. Ask the business what it wants that the system makes difficult, and see which hotspots line up.
  3. Write five characterisation tests around the top candidate.
  4. Sketch where a routing seam could go. If you can't find one, creating it is your first piece of work.