Legacy Code Strangler Pattern for PHP Upgrades
In 1880, the United States Census Bureau faced a growing crisis. The previous census had taken eight years to complete—and with America’s rapid expansion, the next one was projected to take even longer. The data would be obsolete before it was even compiled. They needed a better way, but a complete overhaul was impossible; the census had to continue.
Their solution was not to discard the existing system, but to gradually replace it. They introduced electric tabulating machines that worked alongside traditional methods. Over successive censuses, they phased in more automation, piece by piece, until the old manual processes faded away entirely.
“The task of the programmer is not to create perfection, but to manage transition.” — Martin Fowler
This, in essence, is the story of the Strangler Fig Pattern. The pattern takes its name from the Ficus strangler, a tropical plant that grows around a host tree—but we’ll examine that metaphor more closely in a moment.
And if you’re facing a legacy PHP application—perhaps running on PHP 5.6 or 7.1, with thousands of lines of tangled code and dependencies that can’t be upgraded all at once—you face a similar challenge. You need to modernize without stopping the world.
Why the Strangler Pattern? Understanding Your Options
Before we dive into how to implement the Strangler Fig Pattern, let’s acknowledge an important truth: there’s more than one way to modernize a legacy application. We’ll examine the options briefly, then focus on why the Strangler Pattern often makes sense for PHP upgrades.
You might consider a “big bang” rewrite—building an entirely new application from scratch and switching over all at once. This approach is straightforward in theory but notoriously risky in practice. Kent Beck famously observed that software projects using this approach have a high failure rate; by the time the new system is ready, business requirements have often changed, and the old system’s behavior is difficult to replicate exactly.
Another option is gradual refactoring in place—making incremental improvements directly to the existing codebase without introducing a separate system. This can work for smaller applications, but with PHP upgrades, we often face breaking changes that affect the entire application at once. PHP 7.x removed many deprecated functions; PHP 8.x added strict typing considerations; PHP 8.1 introduced enums and read-only properties; PHP 8.2 brought readonly classes and disallowing dynamic properties. Each version jump can create widespread incompatibilities that are difficult to address piecemeal without a safety net.
The Strangler Fig Pattern offers a third path: we build a new, modern PHP application alongside the old one, and we gradually route functionality from the legacy system to the new system through a proxy layer. The legacy application remains fully functional throughout the process. We migrate one piece at a time—perhaps the user authentication system first, then the product catalog, then the checkout flow—until eventually the legacy application can be decommissioned entirely.
Of course, this pattern isn’t universal. We’ll discuss its limitations later. But for many PHP upgrade scenarios—particularly those involving large, mission-critical applications—it provides a pragmatic balance of safety and progress.
What Is the Strangler Fig Pattern?
Let’s examine the biological metaphor more closely. The pattern takes its name from the Ficus strangler, a tropical plant that grows around a host tree. The fig seed germinates high in the canopy, sends roots down to the ground, and gradually expands until it envelops the host. Eventually, the original tree may die and decay, leaving the fig standing alone—but throughout the process, both trees coexist.
Strictly speaking, the biological process is a form of competition for resources—light, water, nutrients. The fig doesn’t kill the host directly; it simply outcompetes it over time. This distinction matters because in software, we’re not actually trying to harm the legacy system. We’re trying to replace it safely. Though the metaphor isn’t perfect, it captures the essential dynamic: gradual replacement without sudden disruption.
In software, we apply this metaphor by creating a new system that grows around the old one. Rather than replacing the legacy application entirely, we build a routing layer (often a proxy or front controller) that decides, for each incoming request, whether to handle it with the legacy system or the new system. Over time, we move functionality from the legacy side to the new side, piece by piece, until the legacy system is no longer needed.
Strictly speaking, the pattern was named by Martin Fowler, though similar approaches have been used for decades in mainframe migrations and system integration. The key insight is that we can manage the transition risk by keeping both systems operational simultaneously, while steadily expanding the new system’s territory.
When Should You Use the Strangler Pattern?
The Strangler Fig Pattern is particularly well-suited to PHP upgrades under certain conditions. Let’s enumerate when it makes sense—and when it doesn’t.
Strong candidates for the Strangler Pattern:
- Large, monolithic applications where a complete rewrite would take months or years, and business needs can’t wait.
- Applications with continuous uptime requirements—e-commerce sites, SaaS platforms, internal tools that can’t afford extended downtime.
- Systems with complex, poorly understood business logic where rewriting from scratch risks losing critical functionality.
- Projects where you can incrementally deliver value rather than waiting for a “big bang” release.
- Teams transitioning to modern PHP versions where the target version is several major releases ahead (e.g., from PHP 5.6 to PHP 8.2).
- Applications with significant technical debt that would benefit from being rebuilt with modern frameworks and practices.
When to consider alternatives:
- Small applications with limited scope—a full rewrite may be faster and simpler.
- Applications that are already well-architected and can be upgraded with relatively localized changes.
- Projects with unlimited time and budget (rare in practice) where a ground-up rebuild is feasible.
- Systems that are being retired soon—migration effort may not be justified.
- When the legacy codebase is well-tested and the breaking changes are well-documented.
Of course, these categories aren’t mutually exclusive. You might have a large application that’s retiring in six months, or a small application with such severe technical debt that a rewrite is the only sensible option.
The Core Mechanism: How Routing Works
Before we get into the implementation details, let’s understand the fundamental architecture. The Strangler Pattern introduces a routing layer—sometimes called a proxy, a front controller, or a gateway—that sits between the outside world and your applications.
Here’s the basic flow:
Incoming Request → Router/Proxy → Legacy App or New App → Response
The router examines each request (typically by looking at the URL path, but sometimes by examining headers, HTTP method, or other criteria) and decides whether to forward it to the legacy application or the new application. The decision logic can be as simple or as sophisticated as needed:
- A simple route mapping might send
/products/*to the new app and everything else to the legacy app. - A database-driven routing table could allow non-developers to control which routes are migrated via an admin interface.
- Feature flags might gradually shift traffic from legacy to new based on user segments or rollout stages.
- Header-based routing could direct internal staff to the new system while customers remain on the old system.
The key constraint is that the new application must be able to handle some subset of the application’s functionality while leaving the rest to the legacy system. This means the new application needs to share certain infrastructure with the old one—particularly session management, authentication, and potentially database access—at least initially.
Though this introduces some complexity, the isolation also provides safety: problems in the new application don’t necessarily affect the legacy application’s operation.
Step-by-Step Implementation
Let’s walk through implementing the Strangler Pattern in a PHP application. We’ll use a concrete example: imagine you have a legacy e-commerce application running on PHP 7.1 that you need to upgrade to PHP 8.2. The application uses a custom framework and has grown organically over a decade.
Step 1: Establish Your Baseline
Before writing any new code, we need to understand what we’re working with. Ask yourself:
- Which PHP version(s) does the legacy application currently support?
- What are the critical application paths that must remain functional?
- How is the application currently deployed (web server configuration, directory structure, entry points)?
- What shared resources exist (sessions, databases, file storage, caches)?
- What is your current testing coverage?
You should have a way to verify that your legacy application continues to work throughout the migration. This typically means having automated tests—unit tests, integration tests, end-to-end tests—or at minimum, a comprehensive manual test plan.
If you don’t have tests, consider adding some smoke tests before you begin. At a minimum, document the key user workflows (login, browse products, add to cart, checkout, admin functions) and how to verify they work correctly.
Note: If your legacy application has no tests, adding some basic smoke tests before you begin the migration will save you considerable uncertainty later.
Step 2: Set Up the New Application Environment
Create a new PHP application that will eventually replace the legacy system. This is your opportunity to choose modern tools:
- Framework: Laravel, Symfony, Slim, or even a custom approach if that fits your needs. The important thing is that it runs on your target PHP version (e.g., 8.2).
- Dependencies: Use Composer with modern, well-maintained packages. Avoid legacy libraries that would recreate your current problems.
- Testing: Set up PHPUnit or Pest for automated testing from the start.
- Structure: Follow PSR-4 autoloading and modern PHP standards.
For our example, we’ll create a new Laravel application in a subdirectory called new-app/:
cd /var/www/your-project
composer create-project laravel/laravel new-app
You might wonder: does the new application need to replicate all the legacy functionality immediately? No. Start with a minimal application that proves the infrastructure works. We’ll add features as we migrate them.
Step 3: Establish Shared Infrastructure
Before we can route requests to both applications, we need to think about what they share. The most common shared concerns are:
- Sessions: Users may start on the legacy app and continue on the new app without re-authenticating.
- Authentication: Identity should be consistent across both systems.
- Database: Both apps may need read/write access to the same database tables initially.
- File uploads: User-uploaded files need to be accessible to both apps.
The simplest approach is to configure both applications to use the same session storage (Redis, Memcached, or database). Both applications should use the same session cookie name and encryption keys if applicable.
For databases, the new application will typically need to read from the same tables as the legacy application initially. You have a choice:
- Shared database: Both apps read/write the same tables. Simple but creates tight coupling.
- Separate databases with synchronization: More complex but allows the new app to evolve its schema independently.
- Gradual schema migration: Both apps share a database initially, but you add new tables for the new app while keeping legacy tables intact.
For most PHP upgrade scenarios, option 1 (shared database) is the simplest starting point, with the understanding that you’ll eventually migrate the data fully to the new application’s schema.
Step 4: Create the Routing Layer
Now we create the proxy that sits in front of both applications. There are several approaches:
Option A: Modify the web server configuration (Apache/Nginx)
In Nginx, you could use try_files or rewrite rules to send requests to different backends:
server {
listen 80;
server_name yoursite.com;
root /var/www/your-project;
location / {
# Try new app first, then fallback to legacy
try_files $uri @new_app;
}
location @new_app {
# Check if this route has been migrated
if ($request_uri ~* "^/(products|cart|checkout)") {
rewrite ^ /new-app/public/index.php last;
}
# Otherwise go to legacy
rewrite ^ /legacy-app/index.php last;
}
}
This approach is clean and efficient but requires web server configuration changes and potentially reloads.
Option B: Use a PHP front controller proxy
Create a new public/index.php file that becomes the single entry point:
<?php
// public/index.php
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Routes that have been migrated to the new application
$migratedRoutes = [
'/products' => ['new', '/products'],
'/products/' => ['new', '/products/'],
'/cart' => ['new', '/cart'],
'/checkout' => ['new', '/checkout'],
// Add more as you migrate
];
// Determine if this request should go to the new app
foreach ($migratedRoutes as $route => $target) {
if (strpos($requestUri, $route) === 0) {
// Forward to new application
$_SERVER['REQUEST_URI'] = $target[1];
require __DIR__ . '/../new-app/public/index.php';
exit;
}
}
// Default: legacy application
require __DIR__ . '/../legacy-app/index.php';
This vanilla PHP approach is portable and doesn’t require web server configuration changes, though it’s less efficient than the web server approach. It’s a good starting point for proof of concept.
Option C: Use a framework-based router
If you’re using a framework for the new application, you could actually build the router into it:
// new-app/public/index.php (also serving as router)
$router = new Router();
// Define routes that new app handles
$router->get('/products', 'ProductController@index');
$router->post('/cart/add', 'CartController@add');
// ... more routes
// For non-migrated routes, forward to legacy
if (!$router->match($_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD'])) {
chdir(__DIR__ . '/../legacy-app');
require 'index.php';
exit;
}
// Handle migrated routes through new app
$router->dispatch();
This approach is more sophisticated and allows finer-grained control, though it means the front controller is part of the new application codebase.
Which approach should you choose? In my experience, Option B (a dedicated PHP proxy) is often the simplest to implement and understand, especially for teams that aren’t experts in web server configuration. It also makes the routing logic version-controlled and testable as PHP code. The performance difference is usually negligible compared to the application execution time.
Tip: Whatever routing approach you choose, make sure error handling is consistent. If the new application throws an exception, you may want to fall back to the legacy version if possible, or at least display a consistent error page.
Step 5: Migrate Your First Feature
Now comes the actual work: moving functionality from legacy to new. We recommend starting with something relatively isolated—a feature that doesn’t deeply intertwine with the rest of the application.
Let’s migrate a product catalog page as our first example.
In the legacy application, the product catalog is probably handled by something like products.php or a controllers/products module. It likely queries a database table called products and renders templates.
In the new application, we’ll build an equivalent feature using modern Laravel (or whatever framework you chose). The new implementation should:
- Read from the same database table initially (we’ll refactor the data layer later)
- Render using modern templates (Blade, Twig, etc.)
- Expose the same URL path (
/products) so no redirects are needed
Here’s a simple Laravel controller for the migrated product catalog:
<?php
// new-app/app/Http/Controllers/ProductController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use DB; // Using legacy database directly initially
class ProductController extends Controller
{
public function index()
{
// Initially, query the same database as legacy
$products = DB::table('products')
->where('active', 1)
->orderBy('name')
->paginate(20);
return view('products.index', compact('products'));
}
public function show($id)
{
$product = DB::table('products')->find($id);
if (!$product || !$product->active) {
abort(404);
}
return view('products.show', compact('product'));
}
}
And routes:
<?php
// new-app/routes/web.php
use App\Http\Controllers\ProductController;
Route::get('/products', [ProductController::class, 'index']);
Route::get('/products/{id}', [ProductController::class, 'show']);
Then update your router (from Step 4) to include these routes:
$migratedRoutes = [
'/products' => ['new', '/products'],
'/products/' => ['new', '/products/'],
];
Now when users visit /products, they’ll hit the new Laravel application. Other routes continue going to the legacy app.
Testing this change: Before considering the migration complete, verify that:
- The new product pages render correctly
- All links to other parts of the site still work (remember, some links may go to legacy pages)
- Authentication and sessions work across both apps
- The legacy application continues to function for non-migrated routes
Step 6: Iterate
Once the first feature is stable, repeat the process: identify the next piece to migrate, implement it in the new application, update the router, test thoroughly.
Common migration sequencing strategies:
- Start with read-only public pages (home page, product catalog, about pages). These have minimal integration concerns.
- Move to authenticated user areas (user profiles, order history). This tests your session sharing.
- Migrate transactional functionality (shopping cart, checkout). These are more sensitive.
- Handle admin interfaces last, as they often have complex workflows.
- Migrate APIs (if any) once you understand the data models fully.
With each migration, you can gradually shift more data access to the new application’s models and eventually retire direct database access to legacy tables.
Step 7: Decommission the Legacy Application
When all functionality has been migrated and verified, you can remove the legacy application entirely:
- Remove legacy routing fallbacks—all requests go to the new application.
- Remove legacy code and directories.
- Clean up shared infrastructure that was only needed for coexistence (special session configuration, etc.).
- Update any external references (cron jobs, monitoring, deployment scripts).
- Celebrate—you’ve successfully modernized without a risky big bang.
A More Complete Proxy Example
Let’s provide a more robust example of a PHP proxy that includes some real-world considerations:
<?php
// public/index.php - Production-ready proxy with logging and error handling
declare(strict_types=1);
error_reporting(E_ALL);
ini_set('display_errors', '0'); // Disable in production
// Define paths relative to this file
$legacyPath = __DIR__ . '/../legacy-app';
$newAppPath = __DIR__ . '/../new-app/public';
// Define route mappings
$migratedRoutes = [
// Public pages
'/products' => ['new', '/products'],
'/products/' => ['new', '/products/'],
'/about' => ['new', '/about'],
// Authenticated areas
'/account' => ['new', '/account'],
'/account/' => ['new', '/account/'],
'/orders' => ['new', '/orders'],
// API endpoints
'/api/v2/' => ['new', '/api/v2/'],
];
// Normalize request URI (remove query string)
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Check if this request matches a migrated route
$target = null;
foreach ($migratedRoutes as $route => $info) {
if (strpos($requestUri, $route) === 0) {
$target = $info;
break;
}
}
// Logging for debugging (remove in production or use proper logger)
function logRequest(string $destination, string $requestUri): void
{
$logFile = __DIR__ . '/../logs/proxy.log';
$timestamp = date('Y-m-d H:i:s');
$entry = sprintf("[%s] %s -> %s\n", $timestamp, $requestUri, $destination);
file_put_contents($logFile, $entry, FILE_APPEND);
}
try {
if ($target !== null) {
// Route to new application
logRequest('NEW', $requestUri);
// Adjust SCRIPT_FILENAME and REQUEST_URI for the new app
$_SERVER['SCRIPT_FILENAME'] = $newAppPath . '/index.php';
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['PHP_SELF'] = '/index.php';
$_SERVER['REQUEST_URI'] = $target[1] . ($_SERVER['QUERY_STRING'] ? '?' . $_SERVER['QUERY_STRING'] : '');
chdir($newAppPath);
require $newAppPath . '/index.php';
} else {
// Route to legacy application
logRequest('LEGACY', $requestUri);
chdir($legacyPath);
require $legacyPath . '/index.php';
}
} (\Throwable $e) {
// Error handling: log and display friendly error
error_log(sprintf(
"Proxy error for %s: %s in %s:%d\n",
$requestUri,
$e->getMessage(),
$e->getFile(),
$e->getLine()
));
// Optionally fall back to legacy if new app fails
if ($target !== null) {
chdir($legacyPath);
require $legacyPath . '/index.php';
exit;
}
// Display error (in production, show generic error page)
http_response_code(500);
echo "An internal error occurred. The administrator has been notified.";
}
This example adds:
- Logging for visibility into routing decisions
- Proper
$_SERVERvariable adjustment for the routed application - Error handling with fallback to legacy if the new app fails
- Configuration via route mapping array
- Separation of concerns (route definitions, logging, error handling)
Of course, in a real production environment, you might use a dedicated reverse proxy like Nginx with more sophisticated routing rules, or a service mesh. But for many PHP applications, a PHP-based proxy works well and keeps the routing logic in version control alongside your application code.
Common Pitfalls and Gotchas
Even with careful planning, the Strangler Pattern introduces some complexities. Let’s address common issues you might encounter.
Session Incompatibility
If your legacy application stores sessions in files (default PHP behavior) and your new application stores sessions in Redis, users will need to re-authenticate when they hit the new app. The solution is to make both applications use the same session handler from the start. This may require backporting modern session handlers to the legacy app or configuring the legacy app to use the new storage mechanism.
Tip: You can often share sessions by setting the same
session.save_handlerandsession.save_pathin both applications’php.inior runtime configuration, and ensuring they use the samesession.nameand encryption keys if applicable.
CSS/JavaScript Asset Conflicts
If both applications serve static assets from the same URL path (e.g., /css/app.css), you might have collisions. The simplest approach is to serve assets from different paths:
- Legacy assets:
/assets/,/css/,/js/ - New app assets:
/new-assets/,/new-css/,/new-js/
Alternatively, you can route all asset requests to the new application once you’ve migrated to it, but during the transition you’ll need separation.
Absolute URLs in Templates
Legacy templates may contain hard-coded absolute URLs pointing to legacy routes. When those routes are migrated, the links might still point to the old location. You’ll need to:
- Identify all hard-coded URLs in the legacy codebase.
- Make them dynamic (using route helpers) if possible in the legacy templates, OR
- Keep those templates in the legacy application and ensure they only link to legacy routes, not to migrated functionality.
A cleaner approach is to gradually move templates to the new application as you migrate functionality, so the new application’s templating system generates all links for its features.
Database Schema Evolution
Initially, your new application will likely read directly from the legacy database tables. But as you add new features in the new application, you’ll want to create new tables that follow modern conventions (foreign key constraints, proper indexes, consistent naming). This creates a mixed schema.
There’s no perfect solution. Common approaches:
- Keep legacy tables untouched and add new tables with new naming conventions. The new application reads/writes to both.
- Gradually refactor legacy tables from the new application side, adding constraints and normalization over time.
- Use views to provide a stable interface to legacy data while you refactor underlying tables.
Each approach has trade-offs in complexity and risk.
Cookie and Domain Considerations
If your new application lives in a subdirectory (/new-app) but you want clean URLs without the subdirectory, your proxy must handle the URL rewriting carefully. Remember that cookies are typically scoped to a path. If you want sessions to work across both applications, they should share the same cookie path (usually /).
Testing Across Both Applications
Testing becomes more complex when you have two application codebases. We recommend:
- Contract tests: Verify that the new application’s responses match the legacy application’s responses for equivalent requests (at least for initial migration, to ensure you haven’t broken anything).
- Smoke tests across the boundary: Test that a user can log in on the legacy app, add items to their cart on the new app, and checkout on the legacy app without losing session state.
- Automated routing tests: Verify that your proxy correctly routes different URLs to the appropriate backend.
Alternatives and Trade-offs
We’ve focused on the Strangler Fig Pattern, but it’s worth comparing it explicitly with other modernization approaches.
Big Bang Rewrite
What it is: Build an entirely new application and switch over all at once, retiring the legacy system immediately.
Pros:
- No long-term complexity of maintaining two codebases
- No need for proxy/routing layer
- Clean break; no legacy constraints on the new application
- Often faster in retrospect if it succeeds
Cons:
- Very high risk; what if the new application is incomplete or buggy at cutover?
- Long period with no visible progress (can be demoralizing)
- Difficult to replicate all legacy behavior perfectly
- All-or-nothing: if something goes wrong, you may need to roll back completely
When to choose: Small applications, applications with good test coverage that can be replicated accurately, projects with tolerance for extended downtime.
Gradual Refactoring In Place
What it is: Modernize the existing codebase incrementally without introducing a separate new application. Use automated tests, refactoring tools, and modern PHP features directly in the legacy code.
Pros:
- Single codebase to maintain
- No routing complexity
- Changes are often localized
- You can upgrade PHP version by version in place
Cons:
- Working with legacy code can be difficult; tangled dependencies hinder progress
- May be impossible to add tests to sufficiently untested code
- Breaking changes (like PHP 7.x removals) must be addressed throughout the codebase at once
- You’re constrained by the legacy architecture throughout
When to choose: Applications that are relatively well-structured, have good test coverage, and where the PHP version jump isn’t too severe (e.g., PHP 7.4 to PHP 8.0 rather than PHP 5.6 to PHP 8.2).
Parallel Run (Strangler Pattern)
What it is: The pattern we’ve described—new application runs alongside legacy, functionality migrates gradually.
Pros:
- Continuous operation; no big-bang cutover risk
- Value delivered incrementally; stakeholders see progress
- Legacy system remains as fallback
- New application can use modern architecture freely
- You can abort at any time and continue with legacy if needed
Cons:
- Must maintain two applications for an extended period
- Need shared infrastructure (sessions, database) which can be tricky
- Duplicate effort initially (building features that already exist)
- Routing layer adds complexity
- Eventually you must retire the legacy system (which can itself be a project)
When to choose: Large applications, high-availability requirements, significant version jumps, uncertain legacy behavior.
Real-World Considerations
Let’s address some practical concerns that arise in real projects.
How Long Does This Take?
It depends on your team size and application complexity. I’ve seen migrations spanning 3 months for small applications to 18 months for large enterprise systems. The key is to have a reasonable estimate and communicate it to stakeholders.
A useful approach is to migrate one bounded context per sprint. If you’re using a two-week sprint, aim to move one reasonably独立 functional area (e.g., “user profiles,” “product search,” “order management”) in that timeframe. Track the percentage of routes or functionality migrated over time to visualize progress.
Can the New Application Share Models with Legacy?
Initially, you’ll probably query the database directly from the new application to avoid duplicating business logic. But eventually you’ll want to extract that logic into the new application’s models. There’s no perfect way to share code between two separate PHP applications:
- Composer packages: Extract shared logic into internal Composer packages that both applications can require. This works well for business logic that’s relatively independent.
- APIs: Have the new application call the legacy application via HTTP for shared operations. This adds latency but maintains separation.
- Database views: Encapsulate complex queries in database views that both applications can query. This keeps SQL logic in the database where both can access it.
- Duplicate then diverge: Accept that the new application will initially duplicate some logic, then gradually evolve different implementations as the legacy side shrinks.
What About Background Jobs and Cron?
If your legacy application has scheduled tasks (cron jobs, queue workers), you’ll need to migrate those as well. The same strangler principles apply: run both sets of jobs during transition, or route jobs based on what they do. You might:
- Run legacy cron jobs until the functionality they cover has been migrated
- Create a new cron configuration for the new application
- Ensure shared resources (like database locks) don’t conflict
How Do You Handle API Versioning?
If your application exposes APIs to external consumers, you’ll need to version them from the start. The new application could expose /api/v2/ while the legacy app continues serving /api/v1/. Your routing layer can direct API traffic accordingly. Over time, encourage external consumers to migrate to the new API version.
What About Search Functionality?
If your application uses full-text search (Elasticsearch, Algolia, database full-text), you may have indexes built from legacy data. As you migrate features, ensure the new application can search the same data. You might need to:
- Use shared search indexes initially
- Reindex data when the new application’s data model changes
- Consider separate indexes if the new application adds search capabilities the legacy system didn’t have
Measuring Success
How do you know the migration is going well? Track these metrics:
- Percentage of routes handled by new application - the primary progress indicator
- Error rates by application (legacy vs new) - should stay stable or improve
- Performance metrics (response times, database query times) - ensure the new app isn’t slower
- Test coverage - should increase for new code
- Developer velocity - eventually, work on new features should happen primarily in the new application
- Legacy code churn - should decrease as lines of legacy code become static
Set up monitoring and dashboards to track these over time. If you see regression in the legacy application’s metrics after a migration, that may indicate you’ve broken something with shared dependencies.
Conclusion
The Strangler Fig Pattern isn’t a silver bullet, but for many PHP upgrade scenarios it provides a pragmatic path forward. By keeping the legacy application running throughout the transition, we avoid the catastrophic risks of a big bang rewrite. By building a new application alongside it, we gain the benefits of modern PHP versions, frameworks, and practices without waiting for perfection.
Start small: pick an isolated feature, prove the routing and shared infrastructure, then iterate. One piece at a time, the new application grows while the old one shrinks. Eventually, you’ll look back and realize the legacy codebase is gone—not with a bang, but with a whimper.
If you’re facing a legacy PHP upgrade, give the Strangler Pattern consideration. It has served many teams well, from startups to enterprises. And remember: in software, as in nature, sometimes the best way to replace something is to let the new grow around it.
Further Reading:
- Martin Fowler’s original article on the Strangler Fig Pattern
- “Refactoring to Patterns” by Joshua Kerievsky (covers incremental refactoring approaches)
- PHP RFCs for the version you’re targeting (to understand breaking changes)
Note: This article focuses on the pattern itself. Specific PHP version migration guides (e.g., PHP 7.4 → 8.0, PHP 8.0 → 8.1) are beyond our scope but are essential companions to this migration strategy.
Sponsored by Durable Programming
Need help with your PHP application? Durable Programming specializes in maintaining, upgrading, and securing PHP applications.
Hire Durable Programming