# Strangler Fig

## Problem

You have a legacy monolith that needs to be replaced with microservices, but a "big bang" rewrite is too risky. The monolith is running in production, and you can't afford downtime or a long feature freeze. You need to migrate incrementally while keeping the system fully operational.

## Solution

Apply the **Strangler Fig** pattern (named after strangler fig trees that grow around a host tree and eventually replace it):

1. **Intercept** — Place a routing layer (proxy/gateway) in front of the legacy system.
2. **Implement** — Build new functionality as microservices alongside the monolith.
3. **Redirect** — Route specific paths/features to the new service instead of the monolith.
4. **Retire** — Once all traffic for a feature goes to the new service, remove that code from the monolith.

Over time, more and more routes shift to new services until the monolith is fully replaced (or reduced to a minimal core).

## When to Use

- Migrating from a monolith to microservices incrementally.
- The legacy system is too large or risky for a complete rewrite.
- You need zero-downtime migration with rollback capability.
- Features can be isolated by URL path, message type, or domain boundary.

## When to Avoid

- The legacy system is small enough for a clean rewrite in a sprint or two.
- Features are too entangled to route independently.
- There's no clear boundary between old and new (tightly coupled data model).

## Pseudocode

```
class StranglerProxy:
    routes = {
        "/users":   {target: "new-user-service",   migrated: true},
        "/orders":  {target: "new-order-service",   migrated: true},
        "/reports": {target: "legacy-monolith",     migrated: false},
        "/billing": {target: "legacy-monolith",     migrated: false},
    }

    function handle(request):
        route = match(request.path)
        if route.migrated:
            return forward_to_new_service(route.target, request)
        else:
            return forward_to_legacy(request)

    function migrate(path, new_service):
        routes[path] = {target: new_service, migrated: true}
```

## Python

```python
"""Strangler Fig pattern in Python — incremental migration proxy."""
from dataclasses import dataclass, field
from typing import Callable


# ── Request / Response ────────────────────────────────────────

@dataclass
class Request:
    method: str
    path: str
    body: str = ""


@dataclass
class Response:
    status: int
    body: str
    source: str = ""  # which system handled it


# ── Legacy monolith ──────────────────────────────────────────

class LegacyMonolith:
    """Simulates the old system handling all routes."""

    def handle(self, req: Request) -> Response:
        return Response(200, f"[LEGACY] Handled {req.path}", source="legacy")


# ── New microservices ─────────────────────────────────────────

class NewUserService:
    def handle(self, req: Request) -> Response:
        if req.path == "/users/1":
            return Response(200, '{"id":1,"name":"Alice","v":"new"}', source="new-user-svc")
        return Response(404, '{"error":"not found"}', source="new-user-svc")


class NewOrderService:
    def handle(self, req: Request) -> Response:
        return Response(200, '{"orders":[],"v":"new"}', source="new-order-svc")


# ── Strangler Fig Proxy ──────────────────────────────────────

@dataclass
class RouteConfig:
    service: object
    migrated: bool


class StranglerProxy:
    def __init__(self, legacy: LegacyMonolith):
        self.legacy = legacy
        self.routes: dict[str, RouteConfig] = {}
        self.migration_log: list[str] = []

    def register_migration(self, prefix: str, new_service: object) -> None:
        """Migrate a route from legacy to a new service."""
        self.routes[prefix] = RouteConfig(service=new_service, migrated=True)
        self.migration_log.append(f"Migrated {prefix} -> {type(new_service).__name__}")

    def _match_route(self, path: str) -> RouteConfig | None:
        for prefix, config in sorted(self.routes.items(), key=lambda x: -len(x[0])):
            if path.startswith(prefix):
                return config
        return None

    def handle(self, req: Request) -> Response:
        route = self._match_route(req.path)

        if route and route.migrated:
            # Route to new microservice
            return route.service.handle(req)
        else:
            # Fall back to legacy monolith
            return self.legacy.handle(req)

    @property
    def migration_progress(self) -> dict:
        total = len(self.routes) + 1  # +1 for remaining legacy
        migrated = sum(1 for r in self.routes.values() if r.migrated)
        return {"migrated": migrated, "total_routes": total}


# ── Tests ─────────────────────────────────────────────────────

def test_unmigrated_goes_to_legacy():
    proxy = StranglerProxy(LegacyMonolith())
    resp = proxy.handle(Request("GET", "/reports/annual"))
    assert resp.source == "legacy"
    print(f"PASS: unmigrated /reports -> {resp.source}: {resp.body}")


def test_migrated_users_go_to_new_service():
    proxy = StranglerProxy(LegacyMonolith())
    proxy.register_migration("/users", NewUserService())

    resp = proxy.handle(Request("GET", "/users/1"))
    assert resp.source == "new-user-svc"
    assert "Alice" in resp.body
    print(f"PASS: migrated /users/1 -> {resp.source}: {resp.body}")


def test_migrated_orders_go_to_new_service():
    proxy = StranglerProxy(LegacyMonolith())
    proxy.register_migration("/orders", NewOrderService())

    resp = proxy.handle(Request("GET", "/orders"))
    assert resp.source == "new-order-svc"
    print(f"PASS: migrated /orders -> {resp.source}: {resp.body}")


def test_incremental_migration():
    proxy = StranglerProxy(LegacyMonolith())

    # Phase 1: everything goes to legacy
    r1 = proxy.handle(Request("GET", "/users/1"))
    assert r1.source == "legacy"

    # Phase 2: migrate users
    proxy.register_migration("/users", NewUserService())
    r2 = proxy.handle(Request("GET", "/users/1"))
    assert r2.source == "new-user-svc"

    # Phase 3: migrate orders
    proxy.register_migration("/orders", NewOrderService())
    r3 = proxy.handle(Request("GET", "/orders"))
    assert r3.source == "new-order-svc"

    # Reports still on legacy
    r4 = proxy.handle(Request("GET", "/reports"))
    assert r4.source == "legacy"

    print(f"PASS: incremental migration log: {proxy.migration_log}")
    print(f"      progress: {proxy.migration_progress}")


if __name__ == "__main__":
    test_unmigrated_goes_to_legacy()
    test_migrated_users_go_to_new_service()
    test_migrated_orders_go_to_new_service()
    test_incremental_migration()
    print("\nAll Strangler Fig tests passed.")
```

## JavaScript

```javascript
// strangler_fig.js — Incremental migration proxy

// ── Legacy monolith ──────────────────────────────────────────

class LegacyMonolith {
  handle(req) {
    return {
      status: 200,
      body: `[LEGACY] Handled ${req.path}`,
      source: "legacy",
    };
  }
}

// ── New microservices ────────────────────────────────────────

class NewUserService {
  handle(req) {
    if (req.path === "/users/1") {
      return {
        status: 200,
        body: '{"id":1,"name":"Alice","v":"new"}',
        source: "new-user-svc",
      };
    }
    return { status: 404, body: '{"error":"not found"}', source: "new-user-svc" };
  }
}

class NewOrderService {
  handle(req) {
    return {
      status: 200,
      body: '{"orders":[],"v":"new"}',
      source: "new-order-svc",
    };
  }
}

// ── Strangler Fig Proxy ──────────────────────────────────────

class StranglerProxy {
  constructor(legacy) {
    this.legacy = legacy;
    this.routes = new Map();
    this.migrationLog = [];
  }

  registerMigration(prefix, newService) {
    this.routes.set(prefix, { service: newService, migrated: true });
    this.migrationLog.push(
      `Migrated ${prefix} -> ${newService.constructor.name}`
    );
  }

  _matchRoute(path) {
    // Longest prefix match
    const prefixes = [...this.routes.keys()].sort(
      (a, b) => b.length - a.length
    );
    for (const prefix of prefixes) {
      if (path.startsWith(prefix)) return this.routes.get(prefix);
    }
    return null;
  }

  handle(req) {
    const route = this._matchRoute(req.path);
    if (route && route.migrated) {
      return route.service.handle(req);
    }
    return this.legacy.handle(req);
  }
}

// ── Tests ────────────────────────────────────────────────────

function testUnmigratedGoesToLegacy() {
  const proxy = new StranglerProxy(new LegacyMonolith());
  const resp = proxy.handle({ method: "GET", path: "/reports/annual" });
  console.assert(resp.source === "legacy");
  console.log(`PASS: unmigrated /reports -> ${resp.source}: ${resp.body}`);
}

function testMigratedUsersGoToNewService() {
  const proxy = new StranglerProxy(new LegacyMonolith());
  proxy.registerMigration("/users", new NewUserService());

  const resp = proxy.handle({ method: "GET", path: "/users/1" });
  console.assert(resp.source === "new-user-svc" && resp.body.includes("Alice"));
  console.log(`PASS: migrated /users/1 -> ${resp.source}: ${resp.body}`);
}

function testIncrementalMigration() {
  const proxy = new StranglerProxy(new LegacyMonolith());

  // Phase 1: everything legacy
  const r1 = proxy.handle({ method: "GET", path: "/users/1" });
  console.assert(r1.source === "legacy");

  // Phase 2: migrate users
  proxy.registerMigration("/users", new NewUserService());
  const r2 = proxy.handle({ method: "GET", path: "/users/1" });
  console.assert(r2.source === "new-user-svc");

  // Phase 3: migrate orders
  proxy.registerMigration("/orders", new NewOrderService());
  const r3 = proxy.handle({ method: "GET", path: "/orders" });
  console.assert(r3.source === "new-order-svc");

  // Reports still on legacy
  const r4 = proxy.handle({ method: "GET", path: "/reports" });
  console.assert(r4.source === "legacy");

  console.log(
    `PASS: incremental migration log: ${JSON.stringify(proxy.migrationLog)}`
  );
}

testUnmigratedGoesToLegacy();
testMigratedUsersGoToNewService();
testIncrementalMigration();
console.log("\nAll Strangler Fig tests passed.");
```

## Related Patterns

- **Adapter** — the strangler proxy adapts legacy URLs/protocols to new service interfaces during migration.
- **Facade** — the routing proxy acts as a facade, hiding which system (legacy or new) handles each request.
- **Proxy** — the strangler fig routing layer is a proxy that intercepts requests and dispatches to old or new systems.
- **Feature Flag (Strategy)** — feature flags (a strategy pattern application) can gate migration routing per user or environment.
