Essential Microservices Design Patterns
Microservices architecture has become the standard for building scalable, resilient, and maintainable applications. However, distributed systems bring their own challenges. This post explores essential design patterns that help tackle these challenges effectively.
Communication Patterns
1. API Gateway Pattern
The API Gateway acts as a single entry point for all clients. It routes requests to the appropriate microservice, aggregates responses, and handles cross-cutting concerns like authentication and rate limiting.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| @RestController
public class ApiGatewayController {
@Autowired
private ProductService productService;
@Autowired
private ReviewService reviewService;
@GetMapping("/products/{id}")
public ProductDetails getProductDetails(@PathVariable Long id) {
// Get product information
Product product = productService.getProduct(id);
// Get reviews for the product
List<Review> reviews = reviewService.getReviewsForProduct(id);
// Combine data and return
return new ProductDetails(product, reviews);
}
}
|
2. Circuit Breaker Pattern
The Circuit Breaker pattern prevents cascading failures by failing fast when a downstream service is unavailable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| @Service
public class ProductService {
@Autowired
private RestTemplate restTemplate;
@CircuitBreaker(name = "productService", fallbackMethod = "getProductFallback")
public Product getProduct(Long id) {
return restTemplate.getForObject(
"http://product-service/products/" + id,
Product.class
);
}
public Product getProductFallback(Long id, Exception e) {
// Return a default product or cached data
return new Product(id, "Fallback Product", "This is a fallback response", 0.0);
}
}
|
3. Saga Pattern
The Saga pattern manages failures in distributed transactions by defining compensating actions for each step.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| @Service
public class OrderSaga {
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
@Autowired
private ShippingService shippingService;
@Transactional
public OrderResult createOrder(Order order) {
try {
// Step 1: Create an order
OrderEntity savedOrder = orderService.createOrder(order);
try {
// Step 2: Process payment
Payment payment = paymentService.processPayment(order.getCustomerId(), order.getAmount());
try {
// Step 3: Update inventory
inventoryService.updateInventory(order.getItems());
try {
// Step 4: Schedule shipping
Shipping shipping = shippingService.scheduleDelivery(savedOrder);
return new OrderResult(savedOrder, payment, shipping);
} catch (Exception e) {
// Compensate step 3
inventoryService.restoreInventory(order.getItems());
// Compensate step 2
paymentService.refundPayment(payment.getId());
// Compensate step 1
orderService.cancelOrder(savedOrder.getId());
throw e;
}
} catch (Exception e) {
// Compensate step 2
paymentService.refundPayment(payment.getId());
// Compensate step 1
orderService.cancelOrder(savedOrder.getId());
throw e;
}
} catch (Exception e) {
// Compensate step 1
orderService.cancelOrder(savedOrder.getId());
throw e;
}
} catch (Exception e) {
throw new OrderCreationException("Failed to create order", e);
}
}
}
|
Data Management Patterns
1. Database per Service
Each microservice has its own database, ensuring loose coupling and independent development.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Product Service Database (PostgreSQL)
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock INTEGER NOT NULL DEFAULT 0
);
// Order Service Database (MongoDB)
{
"_id": ObjectId("5fbd7c9d5d3b2c1a7c8f4a1b"),
"customerId": "customer123",
"items": [
{ "productId": 101, "quantity": 2, "price": 29.99 },
{ "productId": 102, "quantity": 1, "price": 49.99 }
],
"totalAmount": 109.97,
"status": "PROCESSING",
"createdAt": ISODate("2023-08-24T10:15:30Z")
}
|
2. CQRS (Command Query Responsibility Segregation)
Separate read and write operations for better scalability and performance.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // Command side (Writes)
@RestController
@RequestMapping("/products/commands")
public class ProductCommandController {
@Autowired
private ProductCommandService commandService;
@PostMapping
public ResponseEntity<String> createProduct(@RequestBody ProductCreateCommand command) {
String productId = commandService.createProduct(command);
return ResponseEntity.ok(productId);
}
@PutMapping("/{id}")
public ResponseEntity<Void> updateProduct(@PathVariable String id,
@RequestBody ProductUpdateCommand command) {
commandService.updateProduct(id, command);
return ResponseEntity.ok().build();
}
}
// Query side (Reads)
@RestController
@RequestMapping("/products/queries")
public class ProductQueryController {
@Autowired
private ProductQueryService queryService;
@GetMapping
public ResponseEntity<List<ProductDTO>> getAllProducts() {
return ResponseEntity.ok(queryService.findAllProducts());
}
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProduct(@PathVariable String id) {
return queryService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
|
3. Event Sourcing
Store all changes to the application state as a sequence of events.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| @Service
public class ProductEventSourcingService {
@Autowired
private EventStore eventStore;
@Autowired
private ProductProjection productProjection;
public void createProduct(CreateProductCommand command) {
ProductCreatedEvent event = new ProductCreatedEvent(
command.getProductId(),
command.getName(),
command.getDescription(),
command.getPrice()
);
eventStore.save("product", command.getProductId(), event);
productProjection.apply(event);
}
public void updateProductPrice(UpdateProductPriceCommand command) {
ProductPriceUpdatedEvent event = new ProductPriceUpdatedEvent(
command.getProductId(),
command.getNewPrice()
);
eventStore.save("product", command.getProductId(), event);
productProjection.apply(event);
}
public Product getProduct(String productId) {
List<Event> events = eventStore.getEvents("product", productId);
Product product = new Product(productId);
for (Event event : events) {
if (event instanceof ProductCreatedEvent) {
ProductCreatedEvent e = (ProductCreatedEvent) event;
product.setName(e.getName());
product.setDescription(e.getDescription());
product.setPrice(e.getPrice());
} else if (event instanceof ProductPriceUpdatedEvent) {
ProductPriceUpdatedEvent e = (ProductPriceUpdatedEvent) event;
product.setPrice(e.getNewPrice());
}
// Handle other events
}
return product;
}
}
|
Deployment Patterns
1. Sidecar Pattern
Deploy helper services alongside the main service to handle cross-cutting concerns.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| apiVersion: v1
kind: Pod
metadata:
name: web-app
labels:
app: web
spec:
containers:
- name: main-app
image: my-web-app:latest
ports:
- containerPort: 8080
- name: log-collector
image: log-collector:latest
volumeMounts:
- name: logs
mountPath: /var/log
- name: metrics-collector
image: prometheus-agent:latest
ports:
- containerPort: 9090
volumes:
- name: logs
emptyDir: {}
|
2. Blue-Green Deployment
Maintain two identical production environments to minimize downtime during deployments.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: application-ingress
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: green-service # Current active environment
port:
number: 80
---
# Later, after deploying the blue environment and verifying it works,
# update the ingress to point to the blue service
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: application-ingress
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: blue-service # New active environment
port:
number: 80
|
Resilience Patterns
1. Bulkhead Pattern
Isolate components to prevent failures from cascading through the system.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| @Service
public class ResilientProductService {
@Autowired
private RestTemplate restTemplate;
@Bulkhead(name = "productService", type = Bulkhead.Type.THREADPOOL)
public Product getProduct(Long id) {
return restTemplate.getForObject(
"http://product-service/products/" + id,
Product.class
);
}
@Bulkhead(name = "inventoryService", type = Bulkhead.Type.THREADPOOL)
public Inventory getInventory(Long productId) {
return restTemplate.getForObject(
"http://inventory-service/inventory/product/" + productId,
Inventory.class
);
}
}
|
2. Retry Pattern
Automatically retry failed operations to handle transient failures.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| @Service
public class RetryableOrderService {
@Autowired
private RestTemplate restTemplate;
@Retry(name = "orderService", fallbackMethod = "createOrderFallback")
public Order createOrder(OrderRequest request) {
return restTemplate.postForObject(
"http://order-service/orders",
request,
Order.class
);
}
public Order createOrderFallback(OrderRequest request, Exception e) {
// Save to local queue for later processing
return new Order(null, request.getCustomerId(), "PENDING", new Date());
}
}
|
Conclusion
Microservices design patterns help address the inherent challenges of distributed systems. By understanding and applying these patterns appropriately, developers can build more resilient, scalable, and maintainable microservices architectures.
It’s important to remember that these patterns are tools, not rules. Each has its own trade-offs, and the right pattern depends on your specific requirements and constraints.
In future posts, we’ll dive deeper into implementation details and explore how these patterns can be combined to solve complex architectural problems.