Decide what is actually wrong
"Legacy" bundles several distinct problems with different solutions, and naming yours prevents solving the wrong one:
- Unsupported runtime or framework. A security and hiring problem. Solved by upgrading, which is bounded work.
- Poor structure. Changes are risky because everything is coupled. Solved by refactoring behind tests, incrementally.
- Wrong architecture for current load. Solved by targeted extraction of the parts that need to scale.
- Nobody understands it. A knowledge problem. Solved by documentation and characterisation tests, not by rewriting.
- The business model changed. The only case where a rewrite is sometimes genuinely right, because the data model no longer matches reality.
Teams frequently propose a rewrite for the first or fourth problem, where it is by far the most expensive available option.
Build the safety net first
You cannot safely change a system whose behaviour you cannot verify. Before any restructuring:
Characterisation tests
Not tests of what the system should do — tests of what it currently does, including the odd behaviours. Their purpose is to detect change, and they are what makes refactoring safe rather than hopeful. Where the current behaviour is clearly a bug, note it and keep the test; fixing it is a separate, deliberate decision.
A staging environment with realistic data
Anonymised production-shaped data, not fixtures. Legacy systems break on the messy real records — the account created in 2014 under rules that no longer exist.
Observability on the current system
You need a baseline. Error rates, latency, and throughput before you change anything, or you will not be able to tell whether a change helped.
This phase feels like delay. It is the thing that makes everything after it possible, and skipping it is why incremental efforts turn into rewrites by accident.
The strangler pattern, practically
The general approach is well known: put a routing layer in front, move functionality piece by piece, retire the old code as it is replaced. The practical details determine whether it works.
Start at the edges
Choose a piece with few dependencies and clear boundaries — notifications, reporting, file processing, a public API. Not the core domain. Early wins prove the pattern and build confidence.
Route at a stable boundary
URL paths or API endpoints work well. Routing on database tables or internal function calls creates a coupling that becomes its own problem.
One direction of data flow
The most common way this fails is both systems writing to the same data with different assumptions. Pick an owner for each piece of data. If both need to read it, one owns writes and the other reads — never both writing.
Retire deliberately
Delete the old path once the new one is proven. Systems that keep both alive "just in case" end up permanently maintaining two implementations, which is worse than where they started.
Sequencing that keeps stakeholders on side
Modernisation competes with features for budget, so it has to produce visible value continuously. The order that works:
- Upgrade the runtime and dependencies in place. Unglamorous, bounded, and it removes the security argument immediately.
- Add tests and observability. Enables everything else.
- Extract the piece causing the most operational pain. Whatever wakes someone up or blocks releases. Visible relief buys goodwill for the rest.
- Extract the piece blocking the most roadmap work. Now modernisation is visibly accelerating features.
- Keep going while the return justifies it — and stop when it does not.
That last point matters. The goal is not a fully modern system; it is a system that is not in the way. Plenty of well-run products have an old, stable module nobody touches, and that is a perfectly good outcome.
When a rewrite really is right
To be fair to the option, a rewrite is defensible when several of these hold at once:
- The data model fundamentally does not match what the business now does.
- The system is small enough to rebuild in a few months, not years.
- The requirements are genuinely well understood, because you have been running them for years.
- You can freeze changes to the old system during the rebuild.
- The technology is not merely old but actually unobtainable — no support, no hosting, no hiring pool.
If you cannot freeze the old system, that alone is usually decisive. Chasing a moving target is what turns a six-month rewrite into a two-year one.
The measure that matters
Track one number through a modernisation programme: how long it takes to ship a small change safely, from request to production.
It captures what people actually mean when they say a system is legacy, it is visible to non-technical stakeholders, and it improves incrementally rather than only at the end. If that number is falling, the programme is working — regardless of how much old code remains.