Back to home

Designing Event-Driven Microservices with Spring Cloud Stream

02/04/2024
Microservices
Spring
Event-Driven

Designing Event-Driven Microservices with Spring Cloud Stream

Event-driven architecture is the backbone of modern microservices, enabling loose coupling and scalability. After migrating a monolithic PHP application to event-driven Java microservices, here's what I learned about designing robust event-driven systems.

Why Event-Driven Architecture?

Traditional request-response microservices create tight coupling between services. Event-driven architecture solves this by:

  • Decoupling — Services communicate through events, not direct calls
  • Scalability — Process events independently at different speeds
  • Resilience — Failed events can be retried without affecting other services
  • Auditability — Every state change is an event that can be replayed

Spring Cloud Stream Overview

Spring Cloud Stream provides a opinionated approach to event-driven microservices:

spring:
  cloud:
    stream:
      bindings:
        input-orders:
          destination: orders-topic
          group: order-service
      kafka:
        binder:
          brokers: localhost:9092

Key Patterns

1. Event Sourcing

Instead of storing current state, store all changes as events:

@EventSourcing
public class OrderService {
    
    @Aggregate
    public Order aggregate(OrderCreatedEvent event) {
        return new Order(event.getOrderId(), event.getStatus());
    }
    
    @EventHandler
    public void handle(OrderShippedEvent event) {
        // Update order status
    }
}

2. Sagas for Distributed Transactions

Coordinate long-running transactions across services:

@Saga
public class OrderSaga {
    
    @Start
    public OrderContext start(OrderCreatedEvent event) {
        return new OrderContext(event.getOrderId());
    }
    
    @Step
    public void processPayment(OrderContext context, @StepId("payment") PaymentStartedEvent event) {
        paymentService.processPayment(context.getOrderId());
    }
    
    @Step
    public void shipOrder(OrderContext context, @StepId("shipping") PaymentCompletedEvent event) {
        shippingService.shipOrder(context.getOrderId());
    }
}

3. CQRS (Command Query Responsibility Segregation)

Separate write and read models:

// Write side
@CommandHandler
public void handle(CreateOrderCommand command) {
    Order order = new Order(command.getProductId(), command.getQuantity());
    orderRepository.save(order);
    eventPublisher.publish(new OrderCreatedEvent(order));
}

// Read side
@QueryHandler
public OrderDto query(GetOrderQuery query) {
    return orderQueryRepository.findById(query.getOrderId());
}

Common Pitfalls

1. Event Versioning

Events evolve over time. Always use schema versioning:

public record OrderEventV2(
    String orderId,
    String productId,
    int quantity,
    String customerEmail,
    String version // Always include version
) {}

2. Out-of-Order Events

Network issues can cause events to arrive out of order:

@StreamListener("input-orders")
public void handleOrderEvent(OrderEvent event) {
    // Use sequence numbers to detect and reorder events
    sequenceDetector.process(event.getSequenceNumber(), event);
}

3. Duplicate Events

Events can be delivered multiple times. Make handlers idempotent:

@EventHandler
public void handle(OrderCreatedEvent event) {
    // Check if already processed
    if (processedEvents.contains(event.getEventId())) {
        return; // Idempotent
    }
    processedEvents.add(event.getEventId());
    // Process event
}

Monitoring Event-Driven Systems

Track these metrics:

  • Event throughput — Events processed per second
  • Processing latency — Time from event creation to processing
  • Dead letter queue size — Failed events awaiting manual intervention
  • Consumer lag — Delay between event production and consumption

Technology Stack

  • Spring Cloud Stream — Abstraction over message brokers
  • Apache Kafka — Distributed event streaming platform
  • Spring Kafka — Kafka integration for Spring Boot
  • Avro/Protobuf — Binary serialization for events
  • Schema Registry — Centralized schema management

Conclusion

Event-driven architecture requires careful design around event versioning, idempotency, and ordering. Spring Cloud Stream simplifies the implementation, but the architectural patterns remain complex. Start simple, add complexity as needed, and always prioritize idempotent event handlers.