Production engineering guide for strangler fig migration covering patterns, implementation strategies, and operational best practices.
Strangler Fig Migration is a strategy for incrementally replacing a monolithic system with a modular, service-oriented architecture. It matters when a legacy system is too complex to rewrite in a single release, and you must deliver value continuously while reducing risk. You begin by wrapping the monolith with a new system—like a fig tree encircling a tree trunk—and gradually move functionality from the old to the new, letting the new system grow and take over until the original is fully replaced.
Start with a monolith that handles all business logic, data access, and user interfaces. Deploy a new system—microservices, API gateways, or even a frontend framework—to serve requests alongside the monolith. The new system acts as a "strangler" by intercepting traffic, processing some operations, and delegating others to the monolith. Over time, you replace more components, eventually phasing out the monolith entirely.
The primary entry point to the system is an API gateway. Use it to route requests based on path, header, or query parameters. The gateway must be able to forward requests to both the monolith and the new system.
For example, all requests to /api/v1/users go to the new user service, while /api/v1/orders go to the monolith.
routes:
- path: "/api/v1/users"
target: "http://user-service:8080"
strategy: "proxy"
- path: "/api/v1/orders"
target: "http://monolith:8080"
strategy: "proxy"
- path: "/api/v1/payments"
target: "http://payment-service:8080"
strategy: "proxy"
The gateway must support feature flags or traffic shifting. Use a header-based routing pattern: clients send X-Strangler-Route: user to direct a request to a specific service.
Enable feature toggles to control which parts of the system use the new logic. Use a configuration store—typically a database, Redis, or a file-based system—where feature flags are defined.
A typical feature flag table:
| id | name | enabled | rollout_percentage | created_at |
|---|---|---|---|---|
| 1 | user-profile-api | true | 0.25 | 2024-01-15 |
| 2 | order-creation | false | 1.0 | 2024-01-16 |
When a feature is enabled, the gateway routes traffic to the new service. When rollout_percentage is less than 1.0, traffic is gradually shifted.
For example, to enable a new user profile API for 25% of requests:
POST /api/v1/feature-toggles/user-profile-api
Content-Type: application/json
{
"enabled": true,
"rollout_percentage": 0.25,
"rules": [
{
"type": "header",
"key": "X-User-Type",
"value": "premium",
"weight": 0.5
},
{
"type": "cookie",
"key": "user_id",
"pattern": "^[0-9]{5,}$",
"weight": 0.75
}
]
}
The gateway evaluates rules in order. A request with X-User-Type: premium and user_id=12345 gets a 75% chance to be routed to the new user service.
As services evolve, they must share data. The monolith remains the source of truth for some entities, while new services own others.
Use events to propagate changes. When a new order is created in the new service, emit a OrderCreated event.
{
"event_type": "OrderCreated",
"event_id": "e5f1a3b2-8c4d-9e5f-1a2b-3c4d5e6f7a8b",
"timestamp": "2024-01-17T12:00:00Z",
"data": {
"order_id": 456,
"customer_id": 101,
"items": [
{ "product_id": 203, "quantity": 2 },
{ "product_id": 204, "quantity": 1 }
],
"status": "created"
}
}
The monolith listens to OrderCreated and updates its internal order database. To avoid data inconsistency, use idempotent event handlers.
When a new service fails to process an event, it logs the error and retries using exponential backoff.
Error: Failed to process OrderCreated event e5f1a3b2-8c4d-9e5f-1a2b-3c4d5e6f7a8b
- Retry attempt 1: 2024-01-17T12:01:00Z
- Retry attempt 2: 2024-01-17T12:03:00Z
- Retry attempt 3: 2024-01-17T12:09:00Z
- Final failure: 2024-01-17T12:21:00Z
For critical operations, use dual writes: both the new service and the monolith write data in parallel.
To create a user, the new service sends a CreateUser request to the monolith via HTTP.
POST /api/v1/users
Host: monolith.example.com
Content-Type: application/json
{
"id": 1001,
"name": "Alice Smith",
"email": "[email protected]"
}
The new service also writes to its own database. On success, it emits UserCreated and logs the operation.
If the monolith fails to accept the user, the new service marks the user as "pending" and retries.
When the monolith responds with 409 Conflict, the new service checks the existing user, updates the record, and retries.
{
"error": "User already exists",
"user_id": 1001,
"conflict": {
"version": 2,
"updated_at": "2024-01-17T11:58:00Z"
}
}
The new service must handle duplicate CreateUser requests. It uses a deduplication key—like user_id and email—to prevent creating the same user twice.
As more services grow, they begin to replace monolith logic. The monolith becomes a "backbone"—handling only core, infrequently changing operations.
Start with a simple migration: the new service handles all user-related logic, but the monolith still manages inventory.
Over time, the new service adds order creation, payment processing, and shipping. The monolith is reduced to order orchestration and inventory updates.
When the new service can fully handle order creation, remove the monolith’s order endpoint and replace it with a new one.
GET /api/v1/orders/456
Host: new-order-service.example.com
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
The new service returns a full order with customer, items, payments, and shipping details. The monolith only provides the core order data.
Eventually, the monolith stops serving order creation. It receives updates via OrderUpdated events and responds to OrderStatus queries.
Monitor the migration with a single dashboard tracking:
Use standard tools: Prometheus for metrics, Grafana for dashboards, and Loki for logs.
A critical alert triggers when the new user service fails to process UserCreated events for more than 15 minutes.
# prometheus alert rule: strangler-fig-alerts.yml
groups:
- name: strangler-fig
rules:
- alert: UserServiceEventProcessingDelay
expr: |
rate(event_processing_duration_seconds_sum{job="user-service"}[15m])
/ rate(event_processing_duration_seconds_count{job="user-service"}[15m])
for: 15m
labels:
severity: critical
annotations:
summary: "UserService event processing delay > 15m"
description: |
The average event processing time for UserService has exceeded 15 minutes.
Current average: {{ $value }}
Last 15 minutes of event processing show a backlog of {{ $value }} events.
When a new service fails, the gateway logs the error and redirects traffic to the monolith as a fallback.
When introducing a new service, perform canary deployments. Use traffic shifting to route 10% of requests to the new version.
PUT /api/v1/deployment/243
Content-Type: application/json
{
"version": "2.1.0",
"canary": {
"traffic_percentage": 0.1,
"rollout_start": "2024-01-18T08:00:00Z",
"health_check": {
"path": "/health",
"interval": "30s",
"timeout": "10s"
}
}
}
If health checks fail, the gateway reduces traffic to 5%, then 0%, and reverts to the previous version.
Use feature flags to test new logic in production without full rollout.
When a new service is stable, promote it from canary to full rollout.
X-Strangler-Route header causes all requests to default to the monolith, even if the new service is available. The gateway must log missing headers and fallbacks.OrderCreated event may be processed twice. The system must handle idempotency keys and versioning.After months of gradual replacement, the monolith is reduced to a legacy service, handling only core logic: inventory, billing, and report generation. The new services handle everything else: user profiles, orders, payments, shipping, and analytics.
The API gateway now routes all requests based on feature flags and traffic weights. The system is resilient: if any service fails, the gateway falls back to the monolith, and events continue to be processed.
The monolith is no longer the source of truth but a stable, well-understood system. It is slowly retired, eventually replaced by a new system built from scratch.
The migration is complete. The strangler fig now stands alone, its branches thick with services, its roots deep in the legacy.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy