A legacy PHP app rarely fails all at once. It fails in the margins, where every “small” change breaks something unrelated, releases get scary, and the team starts avoiding improvements because the blast radius is unknown.
The hard part is that a full rewrite is often riskier than the legacy app itself. You trade known problems for unknown ones, burn months before you ship value, and often reintroduce old bugs because the legacy behavior was never fully understood.
The Laravel Strangler Pattern is a practical middle path: you modernize a legacy PHP application by gradually replacing it, feature by feature, while the old system continues to run. Done well, it reduces rewrite risk, improves reliability incrementally, and gives you predictable cutover points.
The strangler pattern (popularized by Martin Fowler as the “Strangler Fig application”) describes an incremental modernization approach:
You keep the legacy app running.
You route a small slice of traffic or functionality to a new system.
You expand those slices over time until the legacy app is mostly gone.
The value is not just technical. It is operational:
Faster time to value: you can ship improvements in weeks, not quarters.
Lower business risk: you can stop or roll back without scrapping the whole effort.
Better decision-making: you learn what the legacy system really does before you replace it.
If you want the canonical definition, Fowler’s write-up is still the best starting point: Strangler Fig Application.
Laravel is a particularly good fit for strangler-style modernization when the existing system is PHP, because you can modernize without forcing a language and ecosystem switch midstream.
Laravel gives you a mature set of building blocks for “new edges” around old systems:
Routing and middleware for clean request handling and cross-cutting concerns (auth, rate limits, logging).
Authentication and authorization primitives that are easier to reason about than many bespoke legacy implementations.
Queues, scheduled jobs, and events to peel off background work from the legacy app.
Testing culture and tooling to lock behavior and prevent regressions as you migrate.
This article is not a general “why Laravel” pitch. Ravenna has a separate piece on Laravel as a replacement framework if you are evaluating the broader decision: Laravel: The Supercharged Framework for Replacing Your Legacy Systems.
Most teams are choosing between two risks: changing the legacy system and rewriting it. The strangler pattern is a way to pay down risk incrementally.
Approach | What you ship early | Risk profile | When it tends to fail |
|---|---|---|---|
Big-bang rewrite | Usually, nothing until late in the project | High, because cutover is all-or-nothing | Requirements drift, missed edge cases, “parallel universe” product decisions |
Strangler pattern | Real production slices quickly (routes, features, jobs) | Moderate, because changes are reversible and scoped | Poor boundaries, inconsistent auth/data ownership, no routing discipline |
“Patch the legacy forever.” | Small fixes only | Hidden high risk, because fragility accumulates | Eventually, you cannot change anything without outages |
If your app is business-critical, the strangler pattern is often the most defensible choice because it supports controlled progress.
Strangler projects fail when teams treat them like a coding exercise. The early work is mostly architecture and operations.
You must be able to decide, at your discretion, whether the legacy app or Laravel handles it. This can happen at different layers:
At the load balancer or ingress level
In a reverse proxy (Nginx/Apache)
At the CDN/edge layer
If you cannot reliably route requests, you cannot reliably modernize in slices.
During migration, you will debug “in between” failures: auth mismatches, session confusion, subtle validation differences, and double writes.
Minimum baseline:
Centralized logs (so you can trace a request across both apps)
Error reporting in both stacks
Performance monitoring (latency per route, slow queries)
Not a 50-page document. A clear stance on:
What will Laravel own first (a route group, a domain module, a background job class)
What the end-state looks like (modular monolith, service boundaries, data ownership)
What you are explicitly not doing (for example, “no microservices during phase 1”)
Without this, strangler migrations become permanent two-headed systems.
Below is a typical approach we use on high-stakes PHP modernizations. Your exact steps vary by architecture and risk tolerance, but the order matters.
Many teams start by listing URLs or screens. That is helpful, but incomplete.
You want to identify seams where functionality can be isolated:
A bounded business capability (billing, reporting, fulfillment)
A set of endpoints with a clear contract
A background process (imports, notifications, reconciliation)
A user workflow that is mostly self-contained
A good first slice has these properties:
Visible business value
Limited dependencies on legacy internals
Minimal shared state
A straightforward rollback path
If you need a forcing function, create a one-page “slice brief” per candidate: owner, routes, data touched, integrations, risks, and acceptance criteria.
There are two common starting topologies.
Laravel becomes the first app to see requests. It either handles them or proxies them to the legacy system.
Pros:
One place to implement cross-cutting concerns (auth, logging, throttling)
You can migrate route-by-route cleanly
Watch-outs:
You must be disciplined about proxy behavior (headers, cookies, caching)
A proxy or ingress routes certain paths to Laravel and everything else to legacy.
Pros:
Simpler boundary at first
Less risk of subtle proxy behavior bugs
Watch-outs:
Cross-cutting concerns can drift between systems
You may end up duplicating auth, rate limits, and logging
Either can work. The difference is less about “best practice” and more about how much operational change your team can safely absorb.
Auth is where strangler projects quietly bleed time.
Common approaches include:
Shared session storage (often Redis) so both apps can validate the same session
Centralized auth (SSO/OAuth) so both apps rely on the same identity provider
Token-based auth for APIs, with clear scopes and rotation
What matters is consistency.
A user who can access a legacy feature must not suddenly get blocked in Laravel.
A user who is blocked in legacy must not gain access in Laravel.
If your legacy app has inconsistent authorization rules, the migration is a chance to fix them, but you need an explicit plan for how you will reconcile differences.
Data is where “incremental” becomes “dangerous” if you are casual.
Here are the most common strategies, along with the trade-offs teams often overlook.
Data strategy | When it works well | Primary risk | Mitigation idea |
|---|---|---|---|
Shared database (Laravel reads and writes legacy tables) | Early slices, when you need speed, and the schema is stable enough | Tight coupling, schema changes become scary | Treat legacy tables as an integration boundary, add tests around critical queries |
Read-only access first (Laravel reads legacy data but writes elsewhere) | Reporting, dashboards, and new admin tools | Incomplete workflows, users need two systems | Start with internal users, or pair with a clear UI transition plan |
Anti-corruption layer (Laravel talks to legacy via an internal API/adapter) | Messy schemas, complex legacy rules | More upfront work | Stabilizes contracts, makes later decoupling easier |
Event-driven replication (legacy publishes events, Laravel builds its own model) | When you need long-term independence | Event correctness and backfills are hard | Start with one domain, build replay tooling early |
A healthy strangler migration can use multiple strategies at once, but each slice should have a declared system of record.
A thin vertical slice is better than a horizontal “layer rewrite.” Instead of rewriting all controllers, then all models, then all views, you migrate a small workflow fully:
Route
Request validation
Authorization
Business logic
Persistence
UI or API response
Instrumentation and logging
This forces you to solve the real integration problems early (auth, session, data assumptions) before the project has too much momentum to change course.
You do not need perfect test coverage of the legacy app to modernize it, but you do need confidence.
Practical rails that work:
Contract tests for the routes you are migrating (request in, response out)
Snapshot tests for critical HTML output if you are migrating server-rendered pages
Database assertions around key invariants (for example, “invoice balance cannot go negative”)
Feature flags so you can toggle Laravel slices per environment, user cohort, or percentage rollout
If you are already seeing fragile architecture and unclear behavior, a structured review can save months. Ravenna’s perspective on that process is here: Laravel Code Audits Spot Risk Before it Ships.
After the first slice, the goal is repeatability.
You want migration standards:
How routing decisions are made
How auth is enforced
How data ownership is declared
How logging correlates across systems
How you deprecate and remove legacy endpoints
This is where senior engineering judgment matters. Most migrations do not fail because Laravel cannot do something. They fail because the organization cannot keep two systems coherent without rules.
A strangler migration only succeeds if the legacy surface area shrinks.
Retirement discipline looks like:
Deprecation dates for old routes
Redirects where appropriate
Removal of unused tables, jobs, and admin screens
Deleting dead code, not just leaving it “for reference.”
If you never delete legacy functionality, you are not strangling. You are just building a second app.
Writing the same business entity in both apps is one of the fastest ways to create data drift.
If you must dual-write temporarily, define:
Which system is the source of truth
How conflicts are detected
How reconciliation happens (manual tooling, scheduled checks, alerts)
Using the legacy DB can be the right move early on, but it should not be the default end state unless you explicitly choose it.
A good compromise is to start with shared tables to ship value, then introduce an adapter layer so Laravel code no longer depends on legacy schema quirks.
Legacy apps often have permissions implemented through ad hoc checks scattered across controllers and templates. If you migrate slices without centralizing authorization, you will create security gaps.
Laravel makes it easier to formalize policies and gates, but you still need to model your permission system deliberately.
Modernization is not just code. It includes release, monitoring, rollback, and incident response.
If your team is relearning deployment while also migrating features, timelines slip. Bake operational improvements into the plan, not as an afterthought.
Ravenna has a broader buyer-focused view of what “real” delivery includes here: Web App Development Services - What You Really Get.
There are cases where incremental modernization is the wrong tool:
The legacy app is small enough that a rewrite is genuinely cheaper and safer.
You cannot get routing control, or you are constrained by a vendor platform.
The business needs a hard pivot in which the old workflows no longer matter.
The legacy system’s core value is embedded in undocumented rules that nobody can validate (in that case, you may need a discovery phase focused on domain behavior before any approach will work).
The strangler pattern is powerful, but it is not magic. It still requires clear boundaries, strong engineering, and a commitment to deleting legacy code.
How long does it take to modernize a legacy PHP app with the Laravel Strangler Pattern? It depends on how quickly you can ship slices safely. Many teams deliver the first meaningful slice in 4 to 10 weeks, then expand in predictable increments. Full retirement can take months to years for complex systems.
Do we need to rewrite the database to use the strangler pattern? Not necessarily. Many successful migrations start by sharing the legacy database for speed. The key is to define data ownership and have a plan to reduce coupling over time.
Can we use the strangler pattern if the legacy app has no tests? Yes, but you should add “migration safety” tests around the slices you touch, such as contract tests and database invariants. You do not need perfect coverage, but you need confidence where it matters.
Is Laravel only useful if we plan to fully replace the legacy app? No. Laravel can also be used to peel off high-risk areas (auth, admin tooling, background jobs) even if the legacy core remains. The important thing is that your scope is intentional.
What is the biggest technical risk in a strangler migration? Data consistency and authorization drift are the top two. Routing is usually solvable. Keeping identity, permissions, and source-of-truth rules coherent across two systems is where projects get expensive.
If you are considering a Laravel strangler migration, the highest-leverage step is often a short technical assessment: identify seams, validate routing and auth options, choose a data strategy for each slice, and produce a plan your team can actually execute.
Ravenna is an official Laravel Partner and works with teams modernizing business-critical platforms. If you want a second opinion on your modernization approach, or you need senior help executing it, start a conversation with us anytime.