Legacy System Modernization: Rewrite, Refactor or Wrap?
Three ways to modernize an old system, when each fits, and how to replace it in slices with the strangler pattern. Includes a decision table and the parts that decide the outcome: data and the parallel run.

A legacy system is software the business depends on and is afraid to change. It still takes the orders or runs the month-end, but every new requirement is slow and risky, and the people who understood it have moved on. Legacy system modernization comes down to three moves: wrap the old system, refactor it, or rewrite it. The projects that succeed usually combine them and replace the system in slices while it keeps running.
This guide explains each move and where it fits, then the two parts that decide the outcome: the data, and the period when old and new run side by side.
Find out what is actually wrong first
"It is old" is not a diagnosis. Different problems call for different treatment, so name the ones you have:
- It is unsafe. The language version, framework or server software no longer receives security fixes. PHP, for example, publishes the end-of-life date of every version, and the systems we are asked to look at are often several versions past it.
- It is slow to change. There are no automated tests, and every fix breaks something else.
- It cannot connect. There is no API, so staff copy data between it and newer tools.
- It cannot keep up. Pages time out at month-end.
Then take an inventory: which features are still used, which reports people rely on, the scheduled jobs, the integrations, and the rules that exist only in the code. That last group is why modernization is harder than it looks. Years of "if the customer is in this region, skip the surcharge" are in there somewhere, and no document lists them.
The three moves
Wrap the system behind an API and leave the core alone
Wrapping means building a thin, modern layer around the old system, usually an API, so that a customer portal, a mobile app or a reporting tool can use its data and functions without touching its insides.
It is the fastest and least risky move. It fits when the core logic is correct and stable and the real problem is that nothing can connect to it. Its limit is that everything behind the wrapper stays as it was: the unsupported runtime, the fragile code, the single server. Wrapping buys time. The liability remains.
Refactor the code where it stands
Refactoring keeps the system and its behavior and improves the inside step by step: upgrade the language and framework to supported versions, add tests around the paths that earn money, pull tangled code into separate modules. The system stays in production, and each step is small enough to roll back.
It fits when the business logic is sound and the platform is still alive, for instance an older PHP or CodeIgniter application that can be brought forward version by version, the route we describe in upgrading a legacy PHP application to PHP 8. The trade-off is that a lot of work may produce little a user can see, which makes it hard to fund.
Rewrite it, but never in one jump
A rewrite builds a new system to take over from the old one. It is justified when the platform is a dead end, when the architecture cannot do what the business now needs, or when the system is small enough to rebuild quickly.
The classic form, in which a team disappears for a long stretch and returns with a replacement to switch on in one night, tends to go badly, for reasons that repeat. The old system keeps changing while the new one is built. The undocumented rules are discovered one angry phone call at a time after launch. The business receives nothing useful until the very end. If you rewrite, do it in increments.
The strangler pattern replaces one slice at a time
The strangler pattern, named after the strangler fig that grows around a host tree until it stands alone, is how an incremental rewrite is organized. You place a routing layer in front of the old system. At first it passes everything through. Then you build one feature in the new system, switch the route for that feature only, and repeat until nothing is left behind the router.
location /reports/ {
proxy_pass http://new-app;
}
location / {
proxy_pass http://legacy-app;
}
In that nginx example, with new-app and legacy-app defined as upstream servers, reporting is already served by the new application and everything else still goes to the old one.
What makes it work in practice:
- Shared login. Nobody should have to sign in twice, so both applications must accept the same session or the same single sign-on.
- A sensible order. Begin with a self-contained, low-risk slice to prove the routing, sign-in and deployment. Then take the slices with the most business value, and put each one into production as it is finished.
- The old code is deleted. A migrated feature that still exists in the legacy system will be used by someone, and you end up maintaining two systems.
Choosing a legacy system modernization approach
| Situation | Usual first move | Why |
|---|---|---|
| Logic is correct and stable, but nothing can integrate with it | Wrap | Fast, low risk, opens the door to new front ends |
| Unsupported version or fragile code, and an upgrade path exists | Refactor, tests first | Removes the security risk and makes later changes cheaper |
| Platform has no future, system is large and business-critical | Rewrite in slices behind a router | Spreads the risk and delivers value early |
| System is small, with few users and simple rules | Direct rewrite | Running two systems is not worth the overhead |
| A product on the market already does the job | Buy it and migrate the data | Building what you can buy rarely pays |
| It works, is isolated and rarely needs to change | Leave it and plan its retirement | Modernization should solve a problem you have |
Most real programs chain these: wrap first so new work can begin, refactor the parts worth keeping, and rewrite the rest slice by slice behind the wrapper.
Data is the hard part
Code can be rewritten from a description. Data has to be carried over exactly, and old databases hold years of exceptions: columns reused for a second purpose, dates stored as text, customers entered three times.
- Decide who owns each table during the transition. The simplest arrangement is that old and new share one database until late in the project. If the new system gets its own, give every piece of data exactly one system that writes it, and sync in one direction. Two systems writing the same records is how data gets lost.
- Profile before you map. Count empty values, duplicates and out-of-range values per column, then decide with the business what to clean and what to archive.
- Script the migration and rehearse it. A script can be run against a copy of production as many times as it takes, and timed. A migration done by hand cannot be tested.
- Reconcile. After each rehearsal, compare row counts, totals (open balances, stock quantities, enrollments) and a sample of individual records.
- Keep the old identifiers. Store the legacy ID on every migrated record, so support staff can still find "order 48213".
The mechanics of moving a live database are in database migration with minimal downtime.
Run old and new in parallel before you switch
A parallel run is the best protection against the undocumented rule. For a period, both systems process the same real inputs and you compare the outputs: the invoices, the stock levels, the reports. Every difference is either a bug in the new system or a rule nobody wrote down, and you find it before customers do.
Keep the old system as the system of record during the run, so a mistake in the new one has no consequences. Cover at least one full business cycle, including a month-end if there is one. Agree in advance what "matching" means, who signs off, and what the route back is: how long the old system stays available read-only, and when returning to it stops being possible.
Doing it in-house or with help
If your own developers know the system and the problem is a version upgrade or a missing API, this can be done in-house, a slice at a time. Outside help is worth it when nobody on staff understands the code, when the system cannot be down during working hours, when the data is messy and financially significant, or when an earlier attempt stalled and left two half-systems.
That is the work our migration and modernization team does, from legacy PHP modernization and moves to Laravel to adding an API layer through our API development service. An audit of code and data is the sensible first step, because it turns "it is old" into a list of specific risks.
Frequently asked questions
Is it cheaper to rewrite or to refactor?
Refactoring is usually cheaper when the platform is still supported and the logic is right, because you keep everything that already works. A rewrite tends to cost more than it first appears, since the old system's hidden rules have to be rediscovered. Rewrite when the platform itself is the problem.
What if nobody understands the old code anymore?
Start by reading it and writing tests that record what it does today, before changing anything. Interview the longest-serving users about exceptions and month-end routines. A parallel run then catches what both the code reading and the interviews missed.
When is it reasonable to leave a legacy system alone?
When it is isolated from the internet, does a stable job, rarely needs changes and can still be restored from backup. Even then, document it, monitor it and decide when it will be retired, so that choice is not made during an outage.
Add worldwincoder.com as a preferred source on Google, or open this article in your AI assistant to use it as a source.
