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 -> monolithNote: 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=/**- Pick a feature with clear boundaries and few writes first (catalogue, notifications).
- Build the new service with its own database; sync data from the monolith with CDC (Debezium) or events during the transition.
- Route a small share of traffic to it, compare results (shadow traffic), then switch fully.
- 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.