Old systems have two things in common: they work, and they make everyone nervous. They work because years of real-world use shaped them around the actual business. They make people nervous because nobody wants to touch them, documentation is thin, and one of the original developers left three jobs ago.
What actually counts as "legacy"
Not every old system is legacy. A 10-year-old application running on a supported stack, well-tested, with active maintenance, is simply mature. Legacy typically means:
- Platform or framework versions that no longer receive security updates.
- Knowledge concentrated in one or two people.
- Fear of changing things because small changes break distant features.
- Manual deployments, manual tests, manual everything.
- A widening gap between what the business needs and what the system can do.
The two failing approaches
- Do nothing. Risk compounds. Eventually something critical breaks at a bad moment.
- Rewrite from scratch. A big-bang rewrite almost always takes longer than planned, discovers hidden requirements too late, and runs in parallel with the old system for so long that the team gives up.
The approaches that actually work sit between these two extremes.
Strategy 1: Strangle the old system
Keep the legacy system running, but gradually route new functionality through a new layer. Over time, more and more features live in the new system; the old one gets smaller until it is finally turned off.
This works for websites (move page by page), internal tools (move module by module) and integrations (introduce a new API gateway, then redirect callers to it).
Strategy 2: Modernise in place
Sometimes the architecture is fine, but the stack is too old. In that case, upgrade one layer at a time: language/runtime, then libraries, then framework, then deployment. Add automated tests as you go, so each step is verifiable. Not glamorous, but very effective.
Strategy 3: Split, then modernise
Large monolithic systems are often easier to modernise piece by piece once the seams are clear. That means first defining boundaries inside the existing system (modules, bounded contexts), then replacing modules independently. The boundaries matter more than the technology choice.
What to do before writing any code
- Write down what the system actually does today, not what it was supposed to do.
- Identify the features people rely on daily, those must keep working through the transition.
- Identify dead features, they're often a meaningful share of the code and should be quietly retired.
- Take inventory of data, integrations, permissions and exports. This is where modernisation projects usually run into trouble.
- Decide what "done" means in measurable terms.
Risk controls that make a real difference
- Backups and tested restores, before any change.
- Automated tests for the behaviour you care about, introduced gradually.
- Feature flags so changes can be rolled back in production without a deploy.
- Observability, logs, error tracking, key business metrics, so you find out quickly if something regressed.
- A staged rollout: new code runs for internal users first, then a small percentage of real users, then everyone.
Strangler fig pattern. Instead of a risky big-bang rewrite, you route traffic through a thin facade that gradually forwards more and more requests to new services, while the legacy system keeps handling the rest until it is empty.
Example: a minimal facade route
A small gateway can decide per-route whether a request goes to the legacy monolith or the new service. Written in a framework-neutral way:
app.use((req, res, next) => {
if (req.path.startsWith('/api/v2/invoices')) {
return proxy(req, res, NEW_SERVICE_URL);
}
return proxy(req, res, LEGACY_URL);
});
Each migrated module moves one prefix from LEGACY_URL to NEW_SERVICE_URL. Users never see a big-bang cutover; the team ships small, reversible steps.
Common trap: writing the new system and the old one as if they must be feature-equivalent on day one. They don't. Start with the smallest piece that has real business value, prove the migration path, then repeat.
Common mistakes
- Making modernisation about the technology choice. Fancy new stacks don't help if the data model and integrations are still mysterious.
- Waiting for a complete redesign before fixing the visible problems. Users pay the cost; momentum dies.
- Turning off the old system too early. Run both in parallel long enough to trust the new one.
- Losing institutional knowledge. Pair with the people who know the system. Their knowledge is the real asset.
A realistic timeline
- Stabilise: backups, monitoring, security patches, minimum tests.
- Map: document what exists and what is actually used.
- Prioritise: pick the piece that is painful now and valuable to modernise first.
- Replace: build the new version of that piece alongside the old.
- Switch: route traffic, monitor, fix issues.
- Retire: decommission the old piece only after it has been quiet for a while.
- Repeat for the next piece.

