Legacy App Modernization: PHP and .NET Without a Rewrite

Legacy app modernization without a full rewrite: renew an old PHP or .NET system with the strangler fig pattern, behaviour tests, an API layer and a clear order.

· 7 min

Picture an order and shipment panel that a logistics company had built in 2014. It runs on PHP 5, its screens were made with jQuery, and the database has passed from one developer to the next over the years. The panel is used every day and does its job. But the server can no longer be updated, the screens break apart on mobile, and every developer who opens the code worries that changing one thing will quietly break another.

At this point the first suggestion is nearly always the same: "Let's rewrite it." The impulse is understandable; the old code is ugly and new code will be clean. In this piece we explain why that decision carries more risk than it appears to, and how you can reach the same place by a safer route, replacing the old system one step at a time. The examples use PHP and .NET Framework, but the approach holds whatever language the system was written in.

The hidden cost of a rewrite

An old application is more than its screens. Over the years, countless small rules have crept in: different payment terms for one group of customers, an account that closes at month end, a warehouse that gets assigned automatically when a field is left empty. Most of these rules appear in no document. They live only in the code, and whoever wrote them moved on long ago.

A system built from scratch includes those rules only if someone remembers them. The forgotten ones surface after go-live, when a customer receives the wrong invoice.

The second problem is time. Until the new system is ready, the old one has to keep running. The business doesn't pause in the meantime: new requests keep arriving and are either added to the old system, leaving the new one behind, or put on hold, leaving the business side impatient. A planned six months often becomes eighteen.

The third problem is cutover day. A single morning on which every user moves from the old system to the new one means the whole project's risk is packed into one day.

The alternative: slowly wrap the old system

In software this is known as the "strangler fig" approach, named after a fig that grows around a host tree and gradually takes its place. The idea is simple. You don't switch the old system off in one go; you put a layer in front of it, move functions to new code one at a time, and retire the old version of each piece once it has moved. Throughout the transition, old and new run side by side.

Back to the logistics panel, it could go like this:

  1. First, the shipment tracking screen is rebuilt with a new interface that reads from the old database.
  2. Users work with the new screen for a few weeks while the old one stays available as a fallback.
  3. If nothing goes wrong, the old tracking screen is switched off and work moves on to the next module.

Every step is small, can be rolled back and delivers value on its own. Users get a better screen to work with long before most of the job is finished.

Step 1: Record today's behaviour first

Before changing any code, you need to capture what the system does today. The most practical way is to write automated tests for the most critical flows: which records are created when an order is entered, how the invoice total is worked out, which tables a return touches.

These tests describe today's behaviour, not the correct behaviour. Their purpose is to let you check, after every change, that the new code produces the same result from the same input. This is also where overlooked business rules tend to come to light, because writing the test naturally raises the question "why is this calculated like that?"

Step 2: Put a gate in the middle

You need a switching point between the old system and the new parts. In a web application this is usually a routing layer: certain URLs go to the new application and everything else goes to the old one. Users believe they are working in a single application.

The data side gets a similar gate. Rather than letting new code connect straight to the old database, it pays to put a small API layer in between. Today the API reads from the old tables; when the data later moves to a new schema, only the inside of the API changes and the screens are left alone.

Step 3: Choose the migration order carefully

Modules are not all worth the same. Two questions help set the order:

  • Where does it hurt most? The screen users complain about most, the one that throws the most errors, or the one that can't be used on mobile at all.
  • Which module has the fewest dependencies? A part that barely touches the other modules and can be moved by itself.

Where the two answers meet is usually a good place to begin. Reporting screens, read-only lists and customer-facing pages often sit right there. Core parts that everything else depends on, such as invoice calculation, wait until the end, once the tests are in place.

Step 4: Close security gaps now

Modernisation can take months, while known vulnerabilities in old versions are a risk today. So some work can come first: upgrading to the nearest supported PHP or .NET version (this often needs fewer changes than people expect), updating plugins and libraries, restricting access to the admin panel, and setting up regular backups along with a test restore from one.

None of this makes the application modern, but it keeps it up and secure while the renewal is under way.

Step 5: Give every migration a finish line

The biggest trap in a gradual migration is that it stalls halfway. The new screen gets built, the old one is left running "just in case", and two years later you are maintaining two systems.

To avoid this, close out each module migration properly: check how much the old screen is still used, switch it off once usage is close to zero, and delete the old code that went with it. A simple table showing where each module stands (old, in transition, new) lets everyone see that the project is actually moving forward.

When is a rewrite the right call?

This approach isn't the best choice every time. Starting from scratch can make more sense when:

  • The application is small enough to be rewritten in a few weeks.
  • The business process is changing from the ground up and most of the old rules no longer hold.
  • The code is so tangled that no layer can be placed in front of it.

Making the call starts with an honest review of the existing system: the code, dependencies, servers, access rights and documentation. A review report means the discussion about which path carries less risk rests on real information rather than opinion.

In short

Rewriting an old PHP or .NET application from scratch looks attractive, but it brings along the business rules buried in the old code, a long stretch of running two systems in parallel and a risky single-day cutover. The safer route: record today's behaviour with tests, put a routing layer and an API layer in between, move modules one at a time in order of pain and dependencies, apply security updates without waiting, and close every migration by deleting the old code.

This is the order we follow for systems we take over through our software maintenance and support service: a written review report first, then security updates and tests for the critical flows, followed by a gradual transition in which the old and new systems run side by side.

Planning a project like this?

Plan it in 10 steps