How to Modernise a Legacy Business Application
Published:
Replace a legacy system incrementally rather than rewriting it in one go. The big rewrite fails for a structural reason, not a technical one: the old system keeps changing while you build its replacement, so you are aiming at a target that moves for as long as the project runs.
Incremental replacement beats the big rewrite
The appeal of a rewrite is obvious. The old code is difficult, the constraints are inherited, and a clean start promises to be faster than untangling what exists. It occasionally is, for small systems with few users and a short feature list.
For a business application that people depend on daily, incremental replacement wins on risk. You deliver value continuously, you find problems while they are small, and at every point you have a working system. Each piece that moves across is independently reversible.
It is slower in total and it is less satisfying, since you spend months running two systems and building the connective tissue between them. That cost is real. It is also much smaller than the cost of a rewrite that is cancelled at eighty per cent complete, which is the characteristic failure of the alternative.
Why rewrites overrun
The old system does not stand still. Regulation changes, a customer needs something, a bug has to be fixed. Every change made to the original is a change the replacement must also make, so the finish line moves away while you walk towards it.
The second reason is that nobody knows everything the old system does. Behaviour accumulated over years is embedded in code nobody has read recently, and much of it exists because of an incident, a regulator or a large customer. It looks like an oddity and is in fact a requirement.
The third is that a rewrite delivers nothing until it delivers everything. There is no partial success to point at, which makes it politically fragile — and when priorities shift, a project with nothing in production is the easiest one to stop.
The strangler pattern in practice
The approach is to put a routing layer in front of the old system and move functionality behind it piece by piece. Requests for the parts that have been rebuilt go to the new system; everything else continues to the old one. Over time the new system handles more, until the old one can be switched off.
Choosing the first piece matters more than anything else in the sequence. It should be genuinely separable, valuable enough to justify the routing infrastructure, and not the most complicated thing in the system. A reporting view or a self-service screen is a far better start than the core transaction engine.
Shared data is where this gets difficult. While both systems are live, something has to be true about where each piece of data lives. The cleanest arrangement is one owner per entity with the other system reading through an interface; two-way synchronisation is sometimes unavoidable and should be treated as the significant piece of engineering it is.
Plan for the end of the sequence from the beginning. Migrations that stall halfway leave an organisation running two systems indefinitely, which is more expensive than either one alone and tends to become permanent through sheer inertia.
Characterisation tests before any change
Before touching legacy code, capture what it currently does. Characterisation tests record actual current behaviour rather than intended behaviour, including the parts that look wrong. They are not a specification of correctness; they are a tripwire for accidental change.
The most practical way to build them is from production data. Capture real inputs and the outputs the system produced, then assert the new implementation matches. Where a difference turns up, it is a conversation — sometimes the old behaviour was a bug, and sometimes it was the requirement nobody documented.
This is also the cheapest way to recover knowledge about a system whose authors have left. The tests end up documenting behaviour that exists nowhere else, which has value well beyond the migration itself.
Rehearsed migration, reversible cutover
Rehearse the data migration against a copy of production, repeatedly, until the run is boring and the duration is known. The first attempt always surfaces data nobody expected. Finding that on a Saturday during cutover is avoidable, and finding it three rehearsals earlier is simply cheaper.
Write the reconciliation checks as part of the migration, not as a separate manual exercise: record counts, financial totals, referential integrity. They should run automatically at the end of every rehearsal, so that a regression in the migration code is caught immediately.
Make the cutover reversible and decide in advance what would trigger a reversal. Keep the old system able to resume for a defined period, and be specific about the conditions — a failed reconciliation, a defect above a stated severity, an elapsed time. Judging it on the night, under pressure, is how organisations end up living with a bad go-live.
When to re-platform instead
Not every legacy system needs replacing. If an application works, is understood, and is not blocking anything the business wants to do, the honest answer may be to leave it alone and spend the money elsewhere. Old is not the same as failing.
Re-platforming — moving the same application onto supported infrastructure, a current runtime or a managed database — addresses the most pressing risks at a fraction of the cost of a rewrite. It buys years, and sometimes that is exactly what is needed.
The signals that genuinely justify replacement are specific: you cannot hire anyone who will work on it, the vendor or runtime is out of support with no upgrade path, security patching is no longer possible, or changes that should take days reliably take months. Absent those, modernise the parts that hurt and leave the rest running.
Related
Discuss a modernisation project
Tell us what you want to build, automate or improve, and we'll help you identify the right technical approach.