The Strangler Fig pattern isn’t about slowly killing off old code; it’s about building a new, more vibrant system around the old one until the old one is entirely bypassed and can be safely removed.
Let’s see this in action. Imagine a monolithic e-commerce application with a ProductService that handles everything from fetching product details to managing inventory. We want to replace this with a new, microservice-based approach.
Here’s a simplified view of the old monolith’s ProductService (conceptually):
// Monolith ProductService (Conceptual)
public class ProductService {
// ... other methods ...
public Product getProductDetails(String productId) {
// 1. Fetch from legacy database
LegacyProductData data = legacyDb.fetchProduct(productId);
// 2. Apply legacy business logic for pricing, discounts, etc.
LegacyPricingEngine.applyDiscounts(data);
// 3. Format for response
return mapToProduct(data);
}
public void updateInventory(String productId, int quantityChange) {
// 1. Lock legacy inventory table
legacyInventoryDb.lockInventory(productId);
// 2. Update inventory in legacy database
legacyInventoryDb.updateQuantity(productId, quantityChange);
// 3. Commit transaction
legacyInventoryDb.commit();
}
}
Now, we want to introduce a new ProductMicroservice that will eventually take over. We’ll start by intercepting requests to getProductDetails.
The Strangler Facade:
We introduce a facade, often an API Gateway or a dedicated proxy layer, that sits in front of both the monolith and the new microservices. Initially, this facade just routes all requests to the monolith.
# API Gateway Configuration (Conceptual)
routes:
- path: /products/**
service: monolith_product_service
Step 1: Extract and Replace a Small Piece
We build a new microservice for getProductDetails. This new service will connect to the same legacy database but apply new logic.
// ProductDetailsMicroservice (Conceptual)
@RestController
@RequestMapping("/api/v2/products")
public class ProductDetailsController {
@Autowired
private ProductRepository productRepository; // Connects to legacy DB
@GetMapping("/{productId}")
public Product getProductDetails(@PathVariable String productId) {
// 1. Fetch from legacy database (same as monolith)
LegacyProductData data = productRepository.findById(productId);
// 2. Apply NEW, modern pricing logic (e.g., a separate microservice)
NewPricingService.calculatePrice(data);
// 3. Format for response
return mapToProduct(data);
}
}
Step 2: Redirect Traffic for That Piece
We update the Strangler Facade to route requests for getProductDetails to our new microservice, while other ProductService calls (like updateInventory) still go to the monolith.
# API Gateway Configuration (Updated)
routes:
- path: /products/{productId} # Specific route for details
service: product_details_microservice
- path: /products/** # Catch-all for other product endpoints
service: monolith_product_service
Now, when a user requests product details, they hit the new microservice. Inventory updates still go to the monolith. The system is running with a mix of old and new.
Step 3: Gradually Replace More
We repeat this process. We might extract inventory management into its own microservice.
// InventoryMicroservice (Conceptual)
@RestController
@RequestMapping("/api/v2/inventory")
public class InventoryController {
@Autowired
private InventoryRepository inventoryRepository; // Connects to legacy DB
@PostMapping("/adjust")
public void adjustInventory(@RequestBody InventoryAdjustmentRequest request) {
// 1. Acquire lock on legacy inventory (or better, a new distributed lock)
inventoryRepository.lockItem(request.getProductId());
// 2. Update inventory in legacy database
inventoryRepository.updateQuantity(request.getProductId(), request.getQuantityChange());
// 3. Commit transaction
inventoryRepository.commit();
}
}
And update the facade again:
# API Gateway Configuration (Further Updated)
routes:
- path: /products/{productId}
service: product_details_microservice
- path: /inventory/adjust # New route for inventory
service: inventory_microservice
- path: /products/** # Still catch-all for any remaining product endpoints
service: monolith_product_service
The key is that the Strangler Facade acts as the single entry point, intelligently directing traffic. Over time, more and more functionality is moved to new services.
The Mental Model:
The Strangler Fig pattern is fundamentally about incrementalism and risk reduction. Instead of a "big bang" rewrite, you build new capabilities alongside the old ones. The facade is the crucial piece that allows you to switch traffic without downtime. Each extracted piece becomes a smaller, more manageable, and more modern service. The monolith’s responsibilities shrink until it’s just a shell, or even completely gone.
The problem this solves is the immense risk, cost, and time associated with a full rewrite. By carving out pieces, you can:
- Reduce Risk: Test and deploy new services independently. If the new
ProductDetailsMicroservicehas a bug, you can quickly revert the facade rule to point back to the monolith. - Deliver Value Sooner: Start seeing the benefits of modern architecture (scalability, independent deployment, technology diversity) on specific features, not just after the entire project is done.
- Modernize Incrementally: Adopt new technologies, databases, or architectural patterns for each new service without affecting the rest of the system.
The "strangling" happens because the new system’s functionality replaces the old system’s functionality, piece by piece. The facade ensures that external clients interact with the new functionality, effectively "strangling" the old paths.
The most surprising aspect is how little the client needs to know. If the facade handles URL mapping and versioning correctly, the client might not even realize they’re talking to different services, or that the underlying architecture has dramatically changed. The facade provides a stable, evolving interface.
The next concept to explore is how to handle data synchronization and consistency between the new microservices and the legacy database during the transition.