Microservices Architecture Patterns

Essential patterns for designing, deploying, and operating microservices: decomposition, communication, data management, and operational patterns.

Microservices architecture structures an application as a collection of loosely coupled, independently deployable services. This guide covers the essential patterns.

Decomposition Patterns

1. Decompose by Business Capability

Organize around business functions, not technical layers.

❌ Technical layers:
  ├─ UI Service
  ├─ Business Logic Service
  └─ Data Access Service

✅ Business capabilities:
  ├─ Order Management
  ├─ Customer Management
  ├─ Inventory Management
  ├─ Payment Processing
  └─ Shipping & Fulfillment

2. Decompose by Subdomain (DDD)

Use Domain-Driven Design to identify bounded contexts.

// Each bounded context = potential microservice
public class OrderContext {      // Order Management
    // Entities: Order, OrderItem, Shipment
    // Aggregates: Order (root)
}

public class CustomerContext {   // Customer Management
    // Entities: Customer, Address, PaymentMethod
    // Aggregates: Customer (root)
}

public class InventoryContext {  // Inventory Management
    // Entities: Product, Warehouse, StockLevel
    // Aggregates: Product (root)
}

3. Strangler Fig Pattern

Incrementally migrate from monolith.

Phase 1:                    Phase 2:                    Phase 3:
┌─────────────┐             ┌─────────────┐             ┌─────────────┐
│  Monolith   │             │  Monolith   │             │  Monolith   │
│             │             │  ┌───────┐  │             │  ┌───────┐  │
│  [Orders]   │──────────▶  │  [Orders]   │──────────▶  │  [Orders]   │
│  [Customer] │  Extract    │  [Customer] │  Extract    │  [Customer] │
│  [Inventory]│             │  [Inventory]│             │  [Inventory]│
└─────────────┘             │  ┌───────┐  │             │  ┌───────┐  │
                            │  │Order Svc│  │             │  │Order Svc│  │
                            │  └───────┘  │             │  └───────┘  │
                            └─────────────┘             └─────────────┘

Communication Patterns

1. Synchronous (Request-Response)

REST/HTTP

GET /api/v1/orders/{orderId}
Authorization: Bearer <token>
Accept: application/json

Response: 200 OK
{
  "orderId": "ord_123",
  "customerId": "cust_456",
  "status": "SHIPPED",
  "items": [...]
}
// order.proto
service OrderService {
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc CreateOrder(CreateOrderRequest) returns (Order);
  rpc StreamOrders(StreamOrdersRequest) returns (stream Order);
}

message Order {
  string order_id = 1;
  string customer_id = 2;
  OrderStatus status = 3;
  repeated OrderItem items = 4;
}

2. Asynchronous (Event-Driven)

// Event publication
public class OrderService {
    private final EventPublisher publisher;
    
    public Order createOrder(CreateOrderCommand cmd) {
        Order order = Order.create(cmd);
        orderRepository.save(order);
        
        // Publish domain event
        publisher.publish(new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getItems(),
            order.getTotal()
        ));
        
        return order;
    }
}

// Event consumption (in Payment Service)
@EventListener
public class PaymentEventHandler {
    private final PaymentService paymentService;
    
    @EventHandler
    public void handle(OrderCreatedEvent event) {
        paymentService.processPayment(
            event.getOrderId(),
            event.getCustomerId(),
            event.getTotal()
        );
    }
}

3. API Gateway Pattern

Single entry point for all clients.

# Kong API Gateway config
services:
  - name: order-service
    url: http://order-service:8080
    routes:
      - name: orders
        paths: ["/api/orders"]
        methods: ["GET", "POST"]
    plugins:
      - name: rate-limiting
        config:
          minute: 1000
          policy: redis
      - name: jwt
      - name: correlation-id

  - name: customer-service
    url: http://customer-service:8080
    routes:
      - name: customers
        paths: ["/api/customers"]

4. Backend for Frontend (BFF)

Tailored APIs per client type.

                    ┌─────────────┐
                    │   Web BFF   │──▶ Web App
                    └──────┬──────┘

              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌─────────┐  ┌─────────┐  ┌─────────┐
        │ Orders  │  │Customer │  │Inventory│
        │ Service │  │ Service │  │ Service │
        └─────────┘  └─────────┘  └─────────┘
              │            │            │
              └────────────┼────────────┘

                    ┌─────────────┐
                    │  Mobile BFF │──▶ Mobile App
                    └─────────────┘

Data Management Patterns

1. Database per Service

Each service owns its data.

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Order     │     │  Customer   │     │ Inventory   │
│  Service    │     │  Service    │     │  Service    │
│  ┌───────┐  │     │  ┌───────┐  │     │  ┌───────┐  │
│  │OrderDB│  │     │  │CustDB │  │     │  │InvDB  │  │
│  └───────┘  │     │  └───────┘  │     │  └───────┘  │
└─────────────┘     └─────────────┘     └─────────────┘

2. Saga Pattern (Distributed Transactions)

// Choreography-based saga
public class OrderSaga {
    private final EventPublisher publisher;
    
    public void createOrder(CreateOrderCommand cmd) {
        // Step 1: Create pending order
        Order order = Order.createPending(cmd);
        orderRepository.save(order);
        
        // Step 2: Reserve inventory (async)
        publisher.publish(new ReserveInventoryCommand(
            order.getId(), order.getItems()
        ));
    }
    
    @EventHandler
    public void handle(InventoryReservedEvent event) {
        Order order = orderRepository.find(event.getOrderId());
        order.confirmInventoryReserved();
        orderRepository.save(order);
        
        // Step 3: Process payment
        publisher.publish(new ProcessPaymentCommand(
            order.getId(), order.getCustomerId(), order.getTotal()
        ));
    }
    
    @EventHandler
    public void handle(PaymentProcessedEvent event) {
        Order order = orderRepository.find(event.getOrderId());
        order.confirmPayment();
        orderRepository.save(order);
        
        // Step 4: Create shipment
        publisher.publish(new CreateShipmentCommand(order));
    }
    
    // Compensating transactions
    @EventHandler
    public void handle(InventoryReservationFailedEvent event) {
        Order order = orderRepository.find(event.getOrderId());
        order.cancel("Inventory unavailable");
        orderRepository.save(order);
    }
}

3. Event Sourcing

Store state changes as events.

// Event-sourced aggregate
public class Order {
    private String orderId;
    private OrderStatus status;
    private List<OrderItem> items;
    private List<DomainEvent> uncommittedEvents = new ArrayList<>();
    
    public static Order create(CreateOrderCommand cmd) {
        Order order = new Order();
        order.apply(new OrderCreatedEvent(
            cmd.getOrderId(), cmd.getCustomerId(), cmd.getItems()
        ));
        return order;
    }
    
    public void addItem(AddItemCommand cmd) {
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException("Cannot modify confirmed order");
        }
        apply(new ItemAddedEvent(orderId, cmd.getItem()));
    }
    
    private void apply(DomainEvent event) {
        when(event);
        uncommittedEvents.add(event);
    }
    
    private void when(OrderCreatedEvent e) {
        this.orderId = e.getOrderId();
        this.status = OrderStatus.PENDING;
        this.items = new ArrayList<>();
    }
    
    private void when(ItemAddedEvent e) {
        this.items.add(e.getItem());
    }
}

4. CQRS (Command Query Responsibility Segregation)

Separate read and write models.

// Write model (commands)
public class OrderCommandHandler {
    private final EventStore eventStore;
    
    @CommandHandler
    public void handle(CreateOrderCommand cmd) {
        Order order = Order.create(cmd);
        eventStore.save(order.getUncommittedEvents());
    }
}

// Read model (queries) - optimized for reading
@Entity
@Table(name = "order_summary")
public class OrderSummary {
    @Id String orderId;
    String customerName;
    OrderStatus status;
    BigDecimal total;
    LocalDateTime createdAt;
    int itemCount;
}

// Projector updates read model
@EventHandler
public class OrderProjector {
    private final OrderSummaryRepository readRepo;
    
    public void on(OrderCreatedEvent event) {
        readRepo.save(new OrderSummary(
            event.getOrderId(),
            event.getCustomerName(),
            OrderStatus.PENDING,
            event.getTotal(),
            event.getTimestamp(),
            event.getItems().size()
        ));
    }
}

Operational Patterns

1. Service Discovery

# Consul service registration
service:
  name: order-service
  port: 8080
  tags: ["v1", "api"]
  check:
    http: http://localhost:8080/health
    interval: 10s
    timeout: 5s

2. Configuration Management

# Spring Cloud Config / Consul Config
order-service:
  database:
    url: ${DATABASE_URL}
    pool-size: 20
  inventory:
    service-url: http://inventory-service
    timeout: 5s
    retry-attempts: 3
  circuit-breaker:
    failure-threshold: 50%
    timeout: 30s

3. Distributed Tracing

// OpenTelemetry instrumentation
@SpanAttribute
public Order getOrder(String orderId) {
    return tracer.spanBuilder("getOrder")
        .setAttribute("order.id", orderId)
        .startScopedSpan(() -> {
            return orderRepository.findById(orderId);
        });
}

4. Health Checks

@Component
public class OrderHealthIndicator implements HealthIndicator {
    private final OrderRepository repository;
    private final InventoryClient inventoryClient;
    
    @Override
    public Health health() {
        // Check database
        if (!repository.isHealthy()) {
            return Health.down()
                .withDetail("database", "unavailable")
                .build();
        }
        
        // Check dependencies
        if (!inventoryClient.isHealthy()) {
            return Health.up()
                .withDetail("inventory", "degraded")
                .withDetail("database", "healthy")
                .build();
        }
        
        return Health.up()
            .withDetail("database", "healthy")
            .withDetail("inventory", "healthy")
            .build();
    }
}

Anti-Patterns to Avoid

Anti-Pattern Problem Solution
Distributed Monolith Services must deploy together Ensure independent deployability
Shared Database Tight coupling, schema conflicts Database per service
Chatty Communication Latency, cascade failures Async events, batch APIs
Synchronous Chains A→B→C→D blocks Event-driven, saga
Data Duplication Inconsistency Single source of truth + events
Nanoservices Operational overhead Right-size services

Technology Choices

Category Options
Service Mesh Istio, Linkerd, Consul Connect
API Gateway Kong, AWS API Gateway, Traefik, Zuul
Service Discovery Consul, etcd, Eureka, Kubernetes DNS
Message Broker Kafka, RabbitMQ, NATS, Pulsar
Distributed Tracing Jaeger, Zipkin, Tempo, AWS X-Ray
Metrics Prometheus + Grafana, Datadog, CloudWatch
Logging ELK/EFK, Loki, Splunk
Deployment Kubernetes, Nomad, ECS, Cloud Run

Further Reading

Related Notes