From Legacy PHP to Modern Laravel 11: The Phased Strangler Fig Migration Playbook
Why 'Big Bang' rewrites fail in 48% of enterprise projects: architecting an incremental Strangler Fig migration using Nginx reverse proxy routing, shared Redis sessions, automated Argon2id password upgrading, and zero-downtime Eloquent legacy schema mapping.

The most perilous decision an engineering leader can make when confronting an aging code repository is authorizing a "Big Bang" rewrite.
A 10-year-old PHP codebase—replete with procedural mysql_* calls, global variables, inline SQL queries, and zero automated test coverage—is undeniably difficult to maintain. Yet that legacy code possesses one overwhelming virtue: it generates the company's revenue and embodies a decade of edge-case business logic that exists nowhere else.
Rewriting the platform from scratch in an isolated branch almost invariably results in catastrophic project failure. While the rewrite team spends 18 months chasing moving requirements, the business must freeze feature delivery, competitors capture market share, and the launch date repeatedly slips. When the cutover finally occurs, hundreds of unmapped domain nuances trigger severe production outages.
THE BIG BANG REWRITE TRAP THE PHASED STRANGLER FIG PATTERN
┌───────────────────────────────────────┐ ┌───────────────────────────────────────┐
│ Isolated Greenfield Rewrite Repo │ │ Unified Edge Reverse Proxy (Nginx) │
│ • 18-month feature freeze │ │ • Traverses routes incrementally │
│ • Moving target requirements │ │ • Legacy PHP serves unmigrated paths │
│ • Institutional knowledge lost │ │ • Modern Laravel 11 serves 400 font-semibold">new domains│
├───────────────────────────────────────┤ ├───────────────────────────────────────┤
│ Result: Budget Overrun (14+ Months) │ │ Time-to-Value: 3 Weeks (First Route) │
│ Cutover Risk: Multi-Day System Outage │ │ Business Interruption: Exactly Zero │
│ Team Morale: Catastrophic Collapse │ │ Defect Containment: Isolated in Route │
└───────────────────────────────────────┘ └───────────────────────────────────────┘
The enterprise-proven alternative is the Strangler Fig Application Pattern, popularized by Martin Fowler. By placing an edge reverse proxy in front of both systems, engineering teams incrementally strangulate legacy procedural routes, replacing them with modern, strongly-typed Laravel 11 and PHP 8.3/8.4 services while users continue operating uninterrupted.
1. Architectural Foundations of the Strangler Fig#
The Strangler Fig pattern derives its name from Australian rainforest vines that seed in the upper branches of host trees, gradually growing downward to envelop the host trunk until the original tree dies away and only the self-supporting fig remains.
In software architecture, this translates to four continuous operational mechanics:
[ Incoming Client Request ]
│
▼
┌───────────────────────────────────────────┐
│ Edge Reverse Proxy (Nginx / OpenResty) │
│ Route Inspection & Session Extraction │
└─────────────────────┬─────────────────────┘
│
┌──────────────────┴──────────────────┐
│ Path Router Engine │
│ Does Laravel handle 400 font-semibold">this route? │
└──────────┬─────────────────┬────────┘
│ YES │ NO
▼ ▼
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ Modern Laravel 11 Cluster │ │ Legacy Procedural PHP Server │
│ • PHP 8.3 Strict Typing │ │ • PHP 7.x Legacy Procedural │
│ • Modern PSR-12 Architecture │ │ • Monolithic Spaghetti Script │
│ • Eloquent ORM + Form Requests │ │ • Raw SQL & Global State │
└────────────────┬────────────────┘ └────────────────┬────────────────┘
│ │
└──────────────────┬──────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Shared Enterprise PostgreSQL / MySQL │
│ Shared Redis Session & Token Store │
└─────────────────────────────────────────┘
- Intercept at the Perimeter: All traffic enters through an intelligent edge router (Nginx, Traefik, or AWS ALB). Neither client devices nor external API consumers are aware that two completely divergent application runtimes coexist.
- Transform in Vertical Slices: Migrate discrete bounded contexts (e.g., User Authentication, Invoicing, Webhooks) rather than horizontal architectural layers (e.g., rewriting the entire database layer first).
- Bridge Session State: Establish seamless single sign-on (SSO) and session sharing across legacy procedural PHP and modern Laravel middleware.
- Decommission and Prune: Once all traffic routes through Laravel 11, the legacy host container is powered down with zero business impact.
2. Step 1: The Edge Reverse Proxy Ingress Router#
The ingress router inspects incoming request URIs and proxies the connection to either the modern Laravel upstream or the legacy backend.
Below is a production Nginx configuration illustrating selective path strangulation with session pass-through:
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic"># /etc/nginx/conf.d/strangler_router.conf
upstream legacy_php_backend {
server 10.0.1.20:9000; 400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic"># Legacy PHP-FPM pool
keepalive 32;
}
upstream modern_laravel_backend {
server 10.0.1.30:9000; 400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic"># Laravel 11 PHP 8.3 FPM pool
keepalive 32;
}
server {
listen 443 ssl http2;
server_name app.knetwork.live;
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic"># SSL hardening parameters
ssl_certificate /etc/ssl/certs/knetwork.crt;
ssl_certificate_key /etc/ssl/400 font-semibold">private/knetwork.key;
root /home/knetwork/apps/router_stub;
index index.php;
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic"># --- STRANGLED ROUTES (Routed to Modern Laravel 11) ---
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic"># Phase 1: Authentication & User Settings
location ~ ^/(login|logout|register|password|api/v1/auth) {
root /home/knetwork/apps/laravel/400 font-semibold">public;
try_files $uri $uri/ /index.php?$query_string;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass modern_laravel_backend;
fastcgi_param SCRIPT_FILENAME /home/knetwork/apps/laravel/400 font-semibold">public/index.php;
fastcgi_param HTTP_X_FORWARDED_BY 400 font-semibold">class="text-emerald-300">"StranglerProxy";
}
}
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic"># Phase 2: High-Velocity Checkout & Invoicing
location ~ ^/(checkout|billing|invoices|api/v1/billing) {
root /home/knetwork/apps/laravel/400 font-semibold">public;
try_files $uri $uri/ /index.php?$query_string;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass modern_laravel_backend;
fastcgi_param SCRIPT_FILENAME /home/knetwork/apps/laravel/400 font-semibold">public/index.php;
}
}
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic"># --- DEFAULT CATCH-ALL (Routed to Legacy PHP Monolith) ---
location / {
root /home/knetwork/apps/legacy_web;
try_files $uri $uri/ /index.php?$query_string;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass legacy_php_backend;
fastcgi_param SCRIPT_FILENAME /home/knetwork/apps/legacy_web$fastcgi_script_name;
fastcgi_param HTTP_X_FORWARDED_BY 400 font-semibold">class="text-emerald-300">"StranglerProxy";
}
}
}
3. Step 2: The Shared Session & Authentication Bridge#
The primary technical blocker in Strangler Fig migrations is session continuity. If a user logs in on the legacy monolith, they must not be asked to log in again when clicking a link that routes to Laravel 11.
3.1 Unifying Session Storage in Redis#
Legacy PHP default file sessions (/var/lib/php/sessions) cannot be read reliably by modern frameworks across distributed nodes. Both systems must be configured to store session data in a shared Redis cluster.In Laravel 11, configure config/session.php:
400 font-semibold">class="text-emerald-300">'driver' => 400 font-semibold">class="text-emerald-300">'redis',
400 font-semibold">class="text-emerald-300">'connection' => 400 font-semibold">class="text-emerald-300">'session',
400 font-semibold">class="text-emerald-300">'cookie' => 400 font-semibold">class="text-emerald-300">'knetwork_session',
400 font-semibold">class="text-emerald-300">'path' => 400 font-semibold">class="text-emerald-300">'/',
400 font-semibold">class="text-emerald-300">'domain' => 400 font-semibold">class="text-emerald-300">'.knetwork.live',
400 font-semibold">class="text-emerald-300">'secure' => 400">true,
400 font-semibold">class="text-emerald-300">'http_only' => 400">true,
400 font-semibold">class="text-emerald-300">'same_site' => 400 font-semibold">class="text-emerald-300">'lax',
3.2 Dual-Hashed Password Upgrading (Argon2id Bridge)#
Legacy PHP systems frequently store password hashes using deprecated algorithms: MD5, SHA-1, or salted SHA-256.Rather than requiring a mass password reset, implement an Automatic Password Re-Hasher inside a custom Laravel Authentication Provider. When a user submits credentials on the login route:
- Verify against modern
password_verify()withArgon2idorBcrypt. - If verification fails, verify against the legacy hashing algorithm (e.g.
hash('sha256', $password . $salt)). - If valid, immediately upgrade the user's password hash in the database to modern Argon2id before completing authentication.
<?php
namespace App\Services\Auth;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
400 font-semibold">class LegacyAuthBridge
{
400 font-semibold">public 400 font-semibold">function validateAndUpgradeCredentials(User $user, 400">string $plainPassword): bool
{
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// 1. Check 400 font-semibold">if password is already modern Bcrypt or Argon2id
400 font-semibold">if (Hash::check($plainPassword, $user->password)) {
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// Check 400 font-semibold">if work factor needs rehashing
400 font-semibold">if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($plainPassword);
$user->save();
}
400 font-semibold">return 400">true;
}
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// 2. Fallback check: Legacy SHA-256 with custom salt
$legacyHash = hash(400 font-semibold">class="text-emerald-300">'sha256', $plainPassword . $user->legacy_salt);
400 font-semibold">if (hash_equals($user->password, $legacyHash)) {
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// Transparently upgrade to modern Argon2id
$user->password = Hash::make($plainPassword);
$user->legacy_salt = 400">null; 400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// Retire legacy salt column
$user->password_upgraded_at = now();
$user->save();
400 font-semibold">return 400">true;
}
400 font-semibold">return 400">false;
}
}
4. Step 3: Database Mapping Without Schema Forking#
Never attempt to run two separate databases during a migration. Maintaining asynchronous replication bridges between a legacy database and a greenfield database creates irreconcilable race conditions and split-brain failures.
Instead, keep the legacy relational schema as the single source of truth, utilizing Laravel’s Eloquent to map onto non-standard database structures:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
400 font-semibold">class CustomerOrder 400 font-semibold">extends Model
{
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// 400">Map to legacy table name
400 font-semibold">protected $table = 400 font-semibold">class="text-emerald-300">'tbl_orders_master_hdr';
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// 400">Map legacy non-standard primary key
400 font-semibold">protected $primaryKey = 400 font-semibold">class="text-emerald-300">'order_pk_id';
400 font-semibold">public $incrementing = 400">true;
400 font-semibold">protected $keyType = 400 font-semibold">class="text-emerald-300">'int';
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// Disable standard created_at / updated_at timestamps
400 font-semibold">public $timestamps = 400">false;
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// Explicitly map legacy column names to clean accessors
400 font-semibold">protected $fillable = [
400 font-semibold">class="text-emerald-300">'cust_fk_id',
400 font-semibold">class="text-emerald-300">'order_total_amt',
400 font-semibold">class="text-emerald-300">'order_status_flg',
400 font-semibold">class="text-emerald-300">'dt_created',
];
400 font-semibold">class=400 font-semibold">class="text-emerald-300">"text-slate-500 italic">// Clean domain accessor
400 font-semibold">public 400 font-semibold">function getTotalAttribute(): float
{
400 font-semibold">return (float) $400 font-semibold">this->attributes[400 font-semibold">class="text-emerald-300">'order_total_amt'];
}
400 font-semibold">public 400 font-semibold">function customer()
{
400 font-semibold">return $400 font-semibold">this->belongsTo(Customer::400 font-semibold">class, 400 font-semibold">class="text-emerald-300">'cust_fk_id', 400 font-semibold">class="text-emerald-300">'cust_id');
}
}
By encapsulating legacy schema ugliness behind clean Laravel Eloquent models and Repository interfaces, your new controllers interact with pristine domain objects. When the legacy system is finally decommissioned, refactoring database column names is an isolated internal task.
5. Operational Benchmark: Big Bang Rewrite vs. Strangler Fig#
The table below contrasts historical project delivery metrics across mid-market enterprise migrations comparing full rewrites with phased Strangler Fig implementations:
| Delivery Metric | "Big Bang" Greenfield Rewrite | Phased Strangler Fig Migration |
|---|---|---|
| Time to First Production Value | 14 to 22 months (All-or-nothing) | 3 weeks (First strangled route) |
| Business Feature Velocity | Frozen (Entire team on rewrite) | Steady (80% features, 20% migration) |
| Production Rollback Granularity | Global revert (High-stakes panic) | Per-Route Nginx switchback (Sub-second) |
| Regression Bug Blast Radius | System-wide (Catastrophic) | Confined strictly to single strangled path |
| Budget Predictability | 2.5× to 4× cost overrun typical | Predictable monthly incremental OPEX |
| Project Cancellation Risk | 48% (Abandoned before launch) | < 2% (Value captured incrementally) |
6. Production Hardening: Automated Fallback Routing#
If a newly strangled route in Laravel encounters an unhandled 500 error in production, the ingress proxy should automatically fall back to the legacy system to protect user revenue.
Below is an OpenResty / Lua configuration snippet implementing zero-downtime automated fallback:
-- /etc/nginx/lua/fallback_router.lua
local res = ngx.location.capture(400 font-semibold">class="text-emerald-300">"/upstream_laravel" .. ngx.400 font-semibold">var.request_uri, {
method = ngx.req.get_method(),
body = ngx.req.get_body_data()
})
-- If modern Laravel responds with a 500 server error, fail over to legacy backend
400 font-semibold">if res.status >= 500 then
ngx.log(ngx.WARN, 400 font-semibold">class="text-emerald-300">"Laravel route failed with status " .. res.status .. 400 font-semibold">class="text-emerald-300">". Falling back to legacy PHP.")
ngx.exec(400 font-semibold">class="text-emerald-300">"/upstream_legacy" .. ngx.400 font-semibold">var.request_uri)
400 font-semibold">else
ngx.status = res.status
400 font-semibold">for k, v in pairs(res.header) do
ngx.header[k] = v
end
ngx.say(res.body)
ngx.exit(res.status)
end
7. Phased 5-Stage Migration Roadmap#
Modernizing legacy infrastructure without technical debt follows an uncompromising operational progression:
Stage 1: Perimeter Setup & Telemetry Baseline (Weeks 1–3)
├── Deploy Nginx / OpenResty reverse proxy in front of legacy monolith
├── Route 100% of traffic through proxy with zero route modifications
└── Profile route traffic volume, error baselines, and dependency graphs
Stage 2: Shared Session & Auth Hardening (Weeks 4–6)
├── Migrate legacy session store to shared Redis cluster
├── Implement dual-hash password upgrade provider (Argon2id bridge)
└── Strangle Authentication routes (/login, /logout, /forgot-password)
Stage 3: Asynchronous Worker Offloading (Weeks 7–10)
├── Decouple cron jobs and background processing 400 font-semibold">from legacy scripts
├── Re-implement email sending and webhooks using Laravel Horizon & Redis queues
└── Verify queue observability and zero-loss job delivery
Stage 4: Core Transactional Domain Strangulation (Weeks 11–20)
├── Migrate high-value business routes (Checkout, Invoicing, Portal Accounts)
├── Enforce strict OpenAPI 3.1 contract testing between legacy and modern APIs
└── Run parallel synthetic traffic tests with automated fallback safety
Stage 5: Final Decommissioning & Schema Refactoring (Weeks 21–24)
├── Verify legacy monolith handles zero production traffic
├── Terminate legacy PHP-FPM pools and decommission legacy server instances
└── Execute clean database migrations to normalize legacy column naming
Modernize Your Enterprise Infrastructure#
For organizations managing complex legacy PHP monoliths, review our core engineering practices across Custom Software Development, Full-Stack Web Development, and Cloud & DevOps Architecture.Explore related systems architecture playbooks including Event Sourcing in Enterprise Laravel, PostgreSQL Partitioning vs. Sharding, and Micro-APIs with Node.js & Redis, or book an architectural consultation with our engineering leadership to evaluate your legacy migration roadmap.
Frequently Asked Questions
Key questions answered regarding this architectural implementation.
Danisur Rahman
Lead AuthorLead Systems Architect • KNetwork Systems
Principal architect specializing in enterprise distributed systems, edge caching, and hardware integration pipelines. Leads engineering audits, high-concurrency database optimizations, and zero-trust VPC deployments across high-growth ventures.
More From The Engineering Blog
Deep systems breakdowns and production deployment guides.
Achieving 100% Mobile Core Web Vitals: Asset Inlining, Font Optimization, and Script Deferral
Hit 100/100 Lighthouse and master Mobile Core Web Vitals on slow 4G cellular links: critical CSS extraction within the 14 KB TCP window, zero-CLS font subsetting with size-adjust fallbacks, web worker script offloading, and long-task yielding.
Server Actions vs. Traditional REST Endpoints: When to Consolidate Client-Server Logic
React Server Actions vs. REST Route Handlers in Next.js 14: how RPC transport serialization, automatic cache revalidation, and zero-bundle mutations reshape modern web architectures without compromising mobile APIs.
Enjoyed this technical breakdown?
Subscribe to receive new architectural guides, system teardowns, and engineering benchmarks directly in your inbox.