The Strangler Fig Pattern: A Safe Path Away from Legacy Monoliths
Your legacy monolith processes millions of euros in transactions every month. It is ugly, slow, and held together by undocumented workarounds that only one developer fully understands—and she left the company in March. The CTO wants to modernize. The board wants to modernize. Everyone agrees the system needs to change.
So the team proposes what feels like the obvious solution: a complete rewrite. New framework, new architecture, new everything. Eighteen months later, the rewrite is behind schedule, the legacy system still handles production traffic, and the team is maintaining two codebases instead of one. This story plays out with depressing regularity across the European software industry.
The strangler fig pattern offers a fundamentally different approach. Instead of replacing the monolith all at once, you grow a new system around it—routing traffic incrementally from old components to new ones until the original system has been entirely replaced. The monolith never stops running. Feature development never stops. And every step of the migration delivers working software in production.
Why Full Rewrites Fail
Before examining the strangler fig pattern in detail, it is worth understanding why the alternative—the full rewrite—fails so consistently.
A working monolith, however ugly, encodes years of business logic, edge cases, and implicit decisions. Much of this logic is not documented. Some of it is not even intentional—it emerged from bug fixes and workarounds that users now depend on. When a team starts a rewrite, they are not just rebuilding the code; they are attempting to reverse-engineer years of institutional knowledge from a system that actively resists being understood.
The result is what Fred Brooks described decades ago and what teams still experience today: the rewrite takes longer than estimated, it misses critical business rules, and by the time it launches, the business requirements have shifted. Meanwhile, the legacy system still needs maintenance, security patches, and occasional feature additions—consuming the same team's time and creating a dual-maintenance burden that drains velocity.
A legacy PHP migration does not have to follow this path. The strangler fig pattern acknowledges a truth that rewrite advocates often overlook: a running system that generates revenue is more valuable than a half-finished replacement that generates nothing.
The Strangler Fig Pattern Explained
The name comes from the strangler fig tree, which grows around a host tree, gradually enveloping it until the host tree dies and the fig stands in its place. The key biological insight that maps to software is that the host tree continues to function throughout the process. It still provides structure and nutrient transport even as the fig grows around it.
In software terms, the pattern works in three phases.
Phase 1: Intercept. Place a routing layer—a reverse proxy, an API gateway, or a middleware component—in front of the monolith. All traffic continues to flow to the legacy system, but the routing layer gives you the ability to redirect specific requests to new services.
Phase 2: Implement. Build a replacement for one bounded context or feature area in the new stack. This replacement must handle the same inputs and produce the same outputs as the legacy component it replaces. Once it passes integration tests against real production data shapes, route traffic for that specific feature from the monolith to the new service.
Phase 3: Retire. Once all traffic for a feature has been routed to the new service and the new service has been stable in production for a defined period, remove the corresponding code from the monolith. The monolith shrinks with each migration cycle.
Repeat phases 2 and 3 until the monolith has no remaining responsibilities and can be decommissioned entirely.
Setting Up the Routing Layer
The routing layer is the critical infrastructure that makes incremental migration possible. Without it, you are forced into a big-bang cutover—which is precisely the risk the strangler fig pattern is designed to avoid.
For PHP monoliths, the most practical approach is an Nginx reverse proxy that routes based on URL path or header values:
# nginx.conf — strangler fig routing
upstream legacy_monolith {
server 10.0.1.10:80;
}
upstream new_order_service {
server 10.0.2.20:8080;
}
server {
listen 80;
server_name app.example.com;
# Migrated: order processing now handled by new service
location /api/orders {
proxy_pass http://new_order_service;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
# Everything else still goes to the monolith
location / {
proxy_pass http://legacy_monolith;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
}
This configuration routes all /api/orders traffic to the new order service while everything else continues to hit the legacy system. Adding a new migration is as simple as adding a new location block. Rolling back is as simple as removing it.
For more granular control—routing based on request headers, user segments, or percentage-based traffic splitting—Traefik or an API gateway like Kong provides richer routing semantics without requiring Nginx configuration reloads.
Choosing What to Migrate First
Not all parts of a monolith are equally good candidates for early migration. The first extraction should ideally be a component that meets three criteria.
Clear domain boundaries. The component has well-defined inputs and outputs, limited shared state with other components, and a coherent business responsibility. An order processing module that reads order requests and writes order records is a better first candidate than a user authentication system that touches every other module.
High change frequency. Components that the business needs to evolve rapidly benefit most from being extracted into a modern codebase. If the product team has a backlog of order-related features but the invoicing module has been stable for two years, start with orders.
Manageable data coupling. Components with their own logical data store or a minimal set of shared tables are easier to extract than components deeply intertwined with a shared database schema. The data migration story matters as much as the code migration story.
A common mistake is choosing the highest-risk component first to "get it over with." This is backwards. The first extraction is where the team learns the migration mechanics—routing, data synchronization, testing strategies, deployment coordination. Doing that learning on the most critical business component is unnecessarily risky. Start with something important enough to justify the effort but contained enough to limit blast radius.
A PHP-to-Symfony Migration Playbook
Consider a concrete scenario: a legacy PHP 5.6 application with no framework, procedural code, and a MySQL database. The team wants to migrate to PHP 8.3 with Symfony 7. Here is how the strangler fig pattern applies.
Step 1: Inventory and Map
Before writing any new code, map the monolith's URL routes to their corresponding PHP files and database tables. In a frameworkless PHP application, this often means reading the .htaccess or Nginx rewrite rules and tracing each URL to its handler script.
# Generate a quick route map from Nginx config or .htaccess
grep -r 'RewriteRule' .htaccess | awk '{print $2, $3}'
# Cross-reference with the database
grep -rn 'mysql_query\|mysqli_query\|PDO' --include='*.php' | head -40
The output gives you a dependency map: which URLs hit which PHP files, which PHP files touch which database tables. This map is the migration planning document.
Step 2: Deploy the Proxy
Set up Nginx or Traefik in front of the monolith. Initially, 100% of traffic passes through to the legacy system. The proxy adds no functionality yet—it establishes the routing layer that future migrations will leverage.
Verify that the proxy introduces no regressions by running your existing test suite (if one exists) or monitoring error rates and response times for 48 hours.
Step 3: Extract the First Service
Build the replacement service in Symfony. For the order processing example:
// src/Controller/OrderController.php
#[Route('/api/orders', name: 'orders_')]
class OrderController extends AbstractController
{
public function __construct(
private readonly OrderService $orderService,
private readonly LoggerInterface $logger,
) {}
#[Route('', methods: ['POST'])]
public function create(Request $request): JsonResponse
{
$payload = json_decode($request->getContent(), true);
try {
$order = $this->orderService->createOrder($payload);
return $this->json($order, Response::HTTP_CREATED);
} catch (ValidationException $e) {
return $this->json(['errors' => $e->getErrors()], Response::HTTP_UNPROCESSABLE_ENTITY);
}
}
#[Route('/{id}', methods: ['GET'])]
public function show(int $id): JsonResponse
{
$order = $this->orderService->findOrFail($id);
return $this->json($order);
}
}
The critical requirement: this service must produce identical responses to the legacy system for the same inputs. Write integration tests that replay real production request/response pairs against the new service to verify behavioral equivalence.
Step 4: Shadow Traffic
Before routing real users to the new service, run shadow traffic: duplicate incoming requests to both the legacy system and the new service, compare responses, but only return the legacy response to the user. This catches behavioral differences without affecting users.
In Nginx, you can achieve this with the mirror directive:
location /api/orders {
mirror /mirror-orders;
proxy_pass http://legacy_monolith;
}
location = /mirror-orders {
internal;
proxy_pass http://new_order_service$request_uri;
}
Log and compare responses from both systems. Fix discrepancies in the new service until it matches the legacy system's behavior for at least 99.9% of requests.
Step 5: Gradual Cutover
Once shadow testing confirms behavioral equivalence, route a small percentage of real traffic to the new service. Start at 5%, monitor error rates and response times, and increase gradually. At Wolf-Tech, we typically follow a 5% → 25% → 50% → 100% progression with 24–48 hours at each stage.
If errors spike at any stage, route traffic back to the monolith immediately. The routing layer makes this a configuration change, not a deployment.
Step 6: Remove Dead Code
Once the new service handles 100% of traffic for the migrated feature and has been stable for at least two weeks, remove the corresponding code from the monolith. This is emotionally satisfying and operationally important—dead code in a monolith creates confusion for developers and increases the surface area for security vulnerabilities.
Handling the Shared Database Problem
The hardest part of the strangler fig pattern is not routing or code extraction—it is the database. Legacy monoliths typically have a single shared database, and the new services need access to the same data.
Three approaches, in order of increasing isolation:
Shared database access. The new service connects to the same database as the monolith. This is the fastest to implement but couples the new service to the legacy schema. It works as a transitional step but should not be the long-term state.
Database view layer. Create views or a read-only replica that presents the legacy data in the shape the new service expects. The new service reads from views and writes through an API that the monolith exposes. This decouples the new service's data model from the legacy schema.
Data synchronization with eventual migration. The new service maintains its own database. A synchronization process (change data capture with Debezium, or event publishing from the monolith) keeps the new database in sync during the transition period. Once all consumers of a data set have been migrated, the synchronization is removed and the new database becomes authoritative.
The right approach depends on the complexity of the data coupling and the timeline of the migration. A legacy code optimization engagement typically starts with shared database access and evolves toward full data isolation as more services are extracted.
Common Mistakes in Strangler Fig Migrations
Migrating too many things at once. The pattern's power comes from its incrementalism. Teams that try to extract five services simultaneously reintroduce the coordination overhead of a big-bang rewrite. Extract one service, stabilize it, learn from the process, then extract the next.
Skipping the shadow traffic phase. Behavioral equivalence testing is not optional. Legacy systems have accumulated years of implicit behavior—edge cases, timezone handling, rounding rules, null value treatment—that documentation does not capture. Shadow traffic reveals these discrepancies before users encounter them.
Neglecting the monolith during migration. The legacy system still serves production traffic during the migration. Deferring all maintenance and security patches on the monolith because "we are replacing it anyway" creates compounding risk. Keep the monolith healthy throughout the process.
No rollback plan. Every traffic routing change should be reversible within minutes. If rolling back requires a deployment, your rollback plan is too slow. Configuration-based routing—not code-level feature flags—provides the fastest rollback path.
How Long Does a Strangler Fig Migration Take?
Timelines vary dramatically based on monolith size, team capability, and the depth of the legacy code's coupling. As a general framework from our experience with European mid-size companies:
A single bounded context extraction—from inventory through shadow traffic to full cutover—typically takes four to eight weeks for a team of two to three senior developers. A monolith with five to eight major bounded contexts can be fully migrated in nine to eighteen months, with each extraction delivering production value along the way.
This is slower than the optimistic timeline of a full rewrite. It is dramatically faster than the realistic timeline, because rewrites almost always take two to three times their initial estimate while the strangler fig approach delivers predictable, incremental progress.
When the Strangler Fig Pattern Is Not the Right Choice
The pattern assumes the existing system is functional and serving users. If the legacy system is fundamentally broken—data corruption, security breaches, or regulatory non-compliance that cannot be patched—a more aggressive approach may be warranted.
Similarly, if the legacy system's data model is so deeply flawed that no new service can work with it, the database migration may dominate the effort to the point where incremental extraction provides little advantage over a structured replacement.
These cases are rare. In most situations, the legacy system works—it is just expensive to maintain, slow to evolve, and running on unsupported infrastructure. The strangler fig pattern is designed precisely for this common scenario.
Conclusion
The strangler fig pattern transforms legacy migration from a high-risk, all-or-nothing gamble into a series of manageable, reversible steps. Each step delivers working software in production. Each step reduces the monolith's footprint. And each step can be paused or accelerated based on business priorities without losing the progress already made.
For CTOs and tech leads managing legacy PHP systems, this approach eliminates the false choice between "live with the monolith forever" and "bet the company on a rewrite." There is a third option: grow the replacement around the existing system, one bounded context at a time, until the monolith is gone and nobody noticed exactly when it disappeared.
If you are evaluating a migration strategy for your legacy system, Wolf-Tech specializes in legacy code optimization and has guided multiple European businesses through strangler fig migrations. Contact us at hello@wolf-tech.io or visit wolf-tech.io for a free initial consultation.

