Home/ Engineering Insights/ Strangler Pattern for Legacy PHP Migration

Running Old and New PHP Side by Side: Routing, Sessions and Rollback

The safest way to replace a legacy system is usually not to replace it. You build the new pieces next to the old ones, send a little traffic to the new code at a time, and retire old code only when nothing uses it. Martin Fowler named this the strangler fig pattern in 2004, after the vines that grow around a tree until the tree is gone.

The idea is easy to state and fiddly in practice. Most of the difficulty isn't in the new code. It's in the seams: how requests find the right system, how users stay logged in across both, who owns which database table, and how you undo a release that went wrong. This post covers those seams.

Put a router in front of both

Everything starts with one entry point that decides which system handles each request. The simplest version is the web server or a reverse proxy, routing by URL path. Here's a minimal nginx example in which /account/ has moved to the new application and everything else still goes to the old one:

server {
    listen 80;
    server_name example.com;

    # migrated routes go to the new application
    location /account/ {
        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # everything else stays on the legacy application
    location / {
        try_files $uri /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

Apache and LiteSpeed can do the same with rewrite and proxy rules. If you can't change the web server, a thin front controller written in PHP can dispatch to the right system, though a proxy is cleaner and faster.

Whatever you use, keep public URLs the same. Visitors, bookmarks and search engines shouldn't notice that anything moved. If a URL has to change, redirect it permanently.

Pick the first piece carefully

Choose something with a clear boundary, moderate traffic and low risk if it goes wrong: a reporting page, a customer account area, a newsletter sign-up. Avoid starting with checkout or anything that moves money. The first slice proves the routing, deployment and monitoring work, and you want to learn that on something forgiving.

Sharing login between old and new

This is the problem that surprises people most. A user logs in through the old system, then clicks a link that the new system serves. The new system has to know who they are.

There are a few ways to handle it:

  • Share the PHP session store. It works if both systems are PHP and you put sessions somewhere common, like Redis. It's also fragile: the two sides must agree on the session data format and the cookie settings, and a framework that stores its own structure inside the session will clash with the old code.
  • Use a signed token or cookie that both sides can verify. The old system issues it at login, and the new system checks the signature using a shared secret or public key. This is cleaner, because neither side depends on the other's internals.
  • Let the old system keep ownership of login and have the new one ask it who the current user is. Simple and safe to start with, but it keeps the new code tied to the old one for longer.

Whichever you choose, test logout, session timeout and password changes across both systems. Those are the paths that get forgotten.

One database, two applications

It's usual for old and new code to talk to the same database, at least at first. That works until both write to the same tables with different assumptions. A few rules help:

  • Make one application the owner of each table. Only it writes there. The other reads, or goes through the owner.
  • When the new code needs data in a better shape, put a small translation layer between the new code and the old schema, so the awkward structure doesn't leak into the new design.
  • Move ownership of tables one at a time, as you migrate the features that use them.

Releases you can undo

Every step above is only safe if you can reverse it. Two habits make that realistic.

Release folders and a symlink. Deploy each version into its own directory, and make the live site a symlink to the current one. Switching versions, forwards or back, becomes a single quick change. Keep the last few releases on disk.

Database changes in two phases. If a migration drops or renames a column, rolling the code back breaks everything. Instead, add the new column first, deploy code that writes to both, copy the existing data across, then switch reads to the new column. Only after the new code has been stable for a while do you remove the old column in a separate release. That way, at every point, both the current code and the previous release work with the schema.

Roll out gradually

You don't need to send everyone to the new code at once. A cookie, a user ID range, or an internal-staff-only rule at the proxy lets you route a small group first. Start with your own team, then a small percentage of users, then everyone. If something's wrong, change the rule back and traffic returns to the old system within seconds.

Watch both systems

Add a response header, such as X-Served-By, so you can tell from the logs which system handled a request. Then compare error rates, response times and key business numbers (orders, sign-ups) between the old and new routes. Problems that come with a migration often show up as a dip in a business metric before any error appears.

Keep the look consistent

Visitors shouldn't see a different design when they cross from one system to the other. Share the header, footer and stylesheet between them, either by serving the same assets from both or by building the shared layout as a separate small component. It's a boring job and it makes the migration feel like one site.

Finishing: delete the old code

The strangler approach only pays off if the old code actually goes away. When the access logs show a legacy route gets no traffic, remove it, together with the tests and configuration that go with it. Otherwise you end up maintaining two systems for good, which is worse than where you started.

Common questions

How long does this take?

It depends on how many routes you move and how tangled the data is. The advantage is that you don't need to know the end date to get value, since each migrated piece improves things immediately.

Do the old and new applications have to use the same language?

No. The proxy doesn't care. Most migrations move from old PHP to a modern PHP framework, but the same approach works for other targets.

Is this the same as microservices?

No. You can migrate piece by piece and still end up with a single application. Splitting into many services is a separate decision; see converting monoliths to microservices.

What should be in place before starting?

Backups, a staging environment and tests around the routes you plan to move. The modernisation roadmap lists the steps, and rewrite or modernise helps with whether to do this at all.

Planning a gradual migration off a legacy system?

We'll help you pick the first slice, set up the routing, and keep every step reversible.

See our legacy system support