1. The Strangler Fig Pattern
Gradually replace specific functions of the monolith with microservices, allowing the two to coexist during migration.
Implementation Steps:
- Identify Boundaries: Define clear service boundaries based on business capabilities
- Create APIs: Build APIs around the identified boundaries within the monolith
- Extract Services: Implement new services that replicate the functionality behind these APIs
- Redirect Traffic: Gradually redirect traffic from the monolith to the new services
- Remove Code: Once a service is fully migrated, remove the corresponding code from the monolith
Example Implementation:
// Step 1: Create an API facade in the monolith
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.getProduct(id);
}
// Other product endpoints...
}
// Step 2: Implement a routing facade that can direct to either the monolith or the new service
@Component
public class ProductRoutingFacade {
@Autowired
private ProductService legacyProductService;
@Autowired
private ProductServiceClient newProductServiceClient;
@Value("${feature.flags.use-new-product-service}")
private boolean useNewProductService;
public Product getProduct(Long id) {
if (useNewProductService) {
return newProductServiceClient.getProduct(id);
} else {
return legacyProductService.getProduct(id);
}
}
// Other product methods...
}
// Step 3: Update the controller to use the routing facade
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductRoutingFacade productFacade;
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productFacade.getProduct(id);
}
// Other product endpoints...
}
2. Domain-Driven Decomposition
Use Domain-Driven Design (DDD) principles to identify bounded contexts that can become microservices.
Implementation Steps:
- Analyze Domain: Identify bounded contexts and aggregates within your domain
- Define Context Maps: Understand relationships between different bounded contexts
- Refactor Monolith: Reorganize the monolith’s internal structure to align with these contexts
- Extract Services: Extract one bounded context at a time into separate services
- Establish Communication: Implement appropriate communication patterns between services
Example Domain Analysis:
E-commerce Domain Bounded Contexts:
1. Product Catalog
- Aggregates: Product, Category, Brand
- Commands: CreateProduct, UpdateProduct, DiscontinueProduct
- Queries: GetProduct, SearchProducts, GetProductsByCategory
2. Order Management
- Aggregates: Order, OrderItem, ShippingInfo
- Commands: CreateOrder, UpdateOrderStatus, CancelOrder
- Queries: GetOrder, GetOrderHistory, GetOrdersByCustomer
3. Customer Management
- Aggregates: Customer, Address, PaymentMethod
- Commands: RegisterCustomer, UpdateCustomerInfo, AddPaymentMethod
- Queries: GetCustomer, GetCustomerOrders, ValidateCustomer
4. Inventory Management
- Aggregates: Inventory, Warehouse, StockMovement
- Commands: ReceiveStock, AllocateStock, AdjustInventory
- Queries: GetProductAvailability, GetWarehouseStock
3. The Sidecar Pattern
Deploy new functionality as services that run alongside the monolith, gradually moving functionality out of the monolith.
Implementation Steps:
- Identify New Features: Select new features that can be implemented as separate services
- Implement Services: Build these features as independent services
- Integrate with Monolith: Use APIs or event-based communication to integrate with the monolith
- Gradually Refactor: Over time, move existing functionality from the monolith to new services
Example Architecture:
# Docker Compose example of sidecar pattern
version: '3'
services:
monolith:
image: e-commerce-monolith:latest
ports:
- "8080:8080"
environment:
- RECOMMENDATION_SERVICE_URL=http://recommendation-service:8081
- SEARCH_SERVICE_URL=http://search-service:8082
depends_on:
- recommendation-service
- search-service
recommendation-service:
image: recommendation-service:latest
ports:
- "8081:8081"
environment:
- SPRING_PROFILES_ACTIVE=production
search-service:
image: search-service:latest
ports:
- "8082:8082"
environment:
- ELASTICSEARCH_URL=http://elasticsearch:9200
depends_on:
- elasticsearch
elasticsearch:
image: elasticsearch:7.10.0
ports:
- "9200:9200"
environment:
- discovery.type=single-node
Real-World Case Studies
Let’s examine how different organizations have approached the monolith vs. microservices decision.
Case Study 1: Amazon’s Journey to Microservices
Initial State: Amazon started with a monolithic application that handled all aspects of their e-commerce platform.
Challenges:
- Scaling the development organization
- Slow deployment cycles
- Difficulty scaling specific components independently
Migration Approach:
- Gradual decomposition into services
- Service-oriented architecture with clear APIs
- “Two-pizza teams” (teams small enough to be fed by two pizzas)
Results:
- Thousands of microservices
- Multiple deployments per second
- Teams can innovate independently
- Highly resilient and scalable platform
Key Lesson: Microservices enabled Amazon to scale both technically and organizationally, but the transition was gradual and pragmatic.
Case Study 2: Shopify’s Modular Monolith
Initial State: Shopify built their e-commerce platform as a Ruby on Rails monolith.
Approach:
- Maintained a monolithic architecture
- Focused on internal modularity
- Invested heavily in tooling and infrastructure
- Extracted specific components as services only when necessary
Results:
- Successfully scaled to support millions of merchants
- Maintained developer productivity
- Avoided unnecessary operational complexity
- Extracted only specific components (e.g., Storefront Renderer) as services
Key Lesson: A well-designed monolith with clear internal boundaries can scale effectively for many organizations.