Skip to content

Strangler Fig Pattern — Incremental Monolith to Microservices Migration Guide

DodaTech Updated 2026-06-24 4 min read

In this tutorial, you'll learn about Strangler Fig Pattern. We cover key concepts, practical examples, and best practices.

The strangler fig pattern incrementally replaces a monolith's functionality with microservices by intercepting requests to specific routes and routing them to new services, allowing migration without downtime or a risky big-bang rewrite.

Why the Strangler Fig Pattern Matters

Rewriting a monolith from scratch is the most common cause of project failure. The monolith keeps accumulating feature requests while the rewrite team falls behind. Amazon's 2001 migration from a monolith to microservices used this pattern — they gradually replaced components while the monolith continued serving the rest. Doda Browser's backend team used the strangler fig pattern to migrate their sync infrastructure from a Rails monolith to Go microservices over 18 months, serving millions of users throughout.

Plain-Language Explanation

A strangler fig is a vine that grows around a host tree. It starts at the base, gradually envelops the trunk, and eventually replaces the tree entirely — while maintaining the tree's shape. The migration pattern works the same way. Start by identifying one route (say /api/v1/users) and build a microservice for it. Place a proxy in front of the monolith that intercepts that specific route and sends it to the new service. Everything else still hits the monolith. Repeat until the monolith is fully replaced.

graph TD
    Client[Client] --> Proxy[Reverse Proxy / Gateway]
    Proxy -->|/api/v1/users| Users[User Microservice]
    Proxy -->|/api/v1/orders| Monolith[Existing Monolith]
    Proxy -->|/api/v1/products| Products[Product Microservice]
    Proxy -->|/api/v1/search| Search[Search Microservice]
    Monolith --> DB[(Monolith DB)]
    Users --> UserDB[(User DB)]
    Products --> ProdDB[(Product DB)]
    Search --> SearchDB[(Search Index)]
    style Monolith fill:#e74c3c,color:#fff
    style Users fill:#27ae60,color:#fff
    style Products fill:#27ae60,color:#fff
    style Search fill:#27ae60,color:#fff
    style Proxy fill:#3498db,color:#fff

Proxy Configuration with NGINX

Route specific paths to new services while sending everything else to the monolith:

upstream monolith {
    server monolith.internal:8080;
}

upstream user_service {
    server user-service:8001;
}

upstream product_service {
    server product-service:8002;
}

server {
    listen 80;

    location /api/v1/users {
        proxy_pass http://user_service;
    }

    location /api/v1/products {
        proxy_pass http://product_service;
    }

    location / {
        proxy_pass http://monolith;
    }
}

Feature Flag Driven Migration

Use feature flags to control which users hit the new service:

import random

class StranglerRouter:
    def __init__(self):
        self.migration_percentage = 10

    def route_users(self, user_id: str) -> str:
        if hash(user_id) % 100 < self.migration_percentage:
            return "user-service"  # new microservice
        return "monolith"

    def migrate_more(self, percentage: int):
        self.migration_percentage = percentage

Data Sync Strategy

During migration, data must be kept in sync between the monolith database and the new service's database:

import asyncio

class DataSync:
    async def initial_load(self, source_db, target_db, table: str):
        rows = source_db.query(f"SELECT * FROM {table}")
        batch = []
        for row in rows:
            batch.append(row)
            if len(batch) >= 100:
                target_db.bulk_insert(table, batch)
                batch = []
        if batch:
            target_db.bulk_insert(table, batch)

    async def cdc_sync(self, source_db, target_db):
        await source_db.listen("data_changes")
        async for change in source_db.changes():
            target_db.apply_change(change)

Common Mistakes

  1. Big-bang rewrite: Trying to replace the entire monolith at once. The risk is enormous — Amazon's 2001 migration took years.

  2. Shared database: The new microservice and monolith sharing the same database creates tight coupling. Extract data ownership as you extract services.

  3. Ignoring data sync: Running monolith and microservice in parallel requires bidirectional data sync. Use CDC (change data capture) tools like Debezium.

  4. No feature parity validation: The new service must handle edge cases the monolith already handles. Run traffic mirroring to compare responses.

  5. Perpetual strangling: Some teams never finish the migration. Set clear milestones and sunset dates for the monolith.

Practice Questions

  1. What is the strangler fig pattern? Incrementally replacing a monolith's functionality by intercepting requests to specific routes and routing them to new microservices, while the monolith continues serving the rest.

  2. How do you validate the new service behaves correctly? Mirror production traffic to both systems and compare responses. Use tools like Diffy or custom response comparators.

  3. What happens to shared data during migration? The monolith database must sync with new service databases. Use change data capture (Debezium) or dual-writes with a transaction outbox pattern.

  4. How do you test the migration safely? Route a small percentage of users to the new service using feature flags. Gradually increase as confidence grows.

  5. When should you stop strangling? When all routes have been migrated and the monolith serves no traffic. Decommission it immediately to prevent accidental rollback.

Mini Project

Build a Python proxy that gradually migrates traffic:

from fastapi import FastAPI, Request
import httpx

app = FastAPI()

class StranglerProxy:
    def __init__(self):
        self.monolith = "http://monolith:8080"
        self.new_services = {
            "/api/v1/users": "http://user-service:8001",
            "/api/v1/products": "http://product-service:8002",
        }
        self.migration_pct = 20

    def should_route_new(self, path: str, user_id: str) -> bool:
        if path not in self.new_services:
            return False
        return hash(user_id) % 100 < self.migration_pct

proxy = StranglerProxy()

@app.api_route("/{path:path}")
async def router(path: str, request: Request):
    user_id = request.headers.get("X-User-Id", "0")
    if proxy.should_route_new(f"/{path}", user_id):
        target = proxy.new_services[f"/{path}"]
    else:
        target = proxy.monolith
    async with httpx.AsyncClient() as c:
        r = await c.request(request.method, f"{target}/{path}")
        return r.json()

Cross-References

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro