Strangler Fig Pattern

Migrating a Monolith Without a Big Rewrite

A strangler fig is a plant that grows around a tree until it replaces it. The strangler fig pattern migrates a monolith the same way: put a routing layer (the API Gateway) in front of it, then move features one at a time into new services. Each step is small, can be rolled back, and the system keeps working.

Step 0             Step 1                       Step 2                     Step N
 clients            clients                      clients                    clients
    |                  |                            |                          |
 monolith         gateway                       gateway                     gateway
                  |-- /api/products -> product-svc   |-- /api/products -> product   |-- all routes -> services
                  '-- everything else -> monolith    |-- /api/orders   -> order     '-- monolith retired
                                                     '-- rest          -> monolith

Note: The strangler fig keeps the business running during a migration that may take months or years. A "big bang" rewrite freezes features for a long time and usually fails at cut-over.

Example: Moving Routes Phase by Phase

Example

The router checks prefixes in order and sends everything else to the monolith. Each phase adds one route; nothing else changes for clients:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        String[] requests = {"GET /api/products/1", "POST /api/orders", "GET /api/users/7",
                             "POST /api/payments", "GET /api/reports/sales"};
        Router router = new Router();
        show("Phase 0: gateway in front of the monolith", router, requests);
        router.add("/api/products", "product-service");
        show("Phase 1: catalogue extracted (read-heavy, low risk)", router, requests);
        router.add("/api/orders", "order-service");
        router.add("/api/payments", "payment-service");
        show("Phase 2: checkout extracted", router, requests);
    }

    static void show(String title, Router router, String[] requests) {
        System.out.println(title);
        for (String r : requests) System.out.printf("  %-24s -> %s%n", r, router.target(r));
        System.out.println();
    }
}

class Router {
    private final Map<String, String> routes = new LinkedHashMap<>();
    void add(String prefix, String service) { routes.put(prefix, service); }
    String target(String request) {
        String path = request.split(" ")[1];
        for (var route : routes.entrySet()) {
            if (path.startsWith(route.getKey())) return route.getValue();
        }
        return "monolith";                      // the catch-all route
    }
}

Output

Phase 0: gateway in front of the monolith
  GET /api/products/1      -> monolith
  POST /api/orders         -> monolith
  GET /api/users/7         -> monolith
  POST /api/payments       -> monolith
  GET /api/reports/sales   -> monolith

Phase 1: catalogue extracted (read-heavy, low risk)
  GET /api/products/1      -> product-service
  POST /api/orders         -> monolith
  GET /api/users/7         -> monolith
  POST /api/payments       -> monolith
  GET /api/reports/sales   -> monolith

Phase 2: checkout extracted
  GET /api/products/1      -> product-service
  POST /api/orders         -> order-service
  GET /api/users/7         -> monolith
  POST /api/payments       -> payment-service
  GET /api/reports/sales   -> monolith

The Same Thing with Spring Cloud Gateway

api-gateway

Routes are checked by order; the monolith is the lowest-priority catch-all:

application.yml

spring:
  cloud:
    gateway:
      routes:
        - id: products
          uri: lb://product-service
          order: 1
          predicates:
            - Path=/api/products/**
        - id: orders-new                    # move checkout traffic gradually
          uri: lb://order-service
          order: 2
          predicates:
            - Path=/api/orders/**
            - Weight=orders-group, 10       # 10% to the new service...
        - id: orders-legacy
          uri: http://legacy-shopease:8080
          order: 2
          predicates:
            - Path=/api/orders/**
            - Weight=orders-group, 90       # ...90% still to the monolith
        - id: monolith
          uri: http://legacy-shopease:8080
          order: 100
          predicates:
            - Path=/**
  1. Pick a feature with clear boundaries and few writes first (catalogue, notifications).
  2. Build the new service with its own database; sync data from the monolith with CDC (Debezium) or events during the transition.
  3. Route a small share of traffic to it, compare results (shadow traffic), then switch fully.
  4. Delete the old code in the monolith. Repeat.

Tip: Add an anti-corruption layer in the new service: a small adapter that translates the monolith's old data model (for example TBL_PRD.PRD_NM) into your clean new model, so legacy design does not leak into the new code.