spring-boot-event-driven-patterns

📁 giuseppe-trisciuoglio/developer-kit 📅 10 days ago
101
总安装量
101
周安装量
#2271
全站排名
安装命令
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-boot-event-driven-patterns

Agent 安装分布

claude-code 73
replit 65
cursor 64
opencode 61
antigravity 58

Skill 文档

Spring Boot Event-Driven Patterns

Overview

Implement Event-Driven Architecture (EDA) patterns in Spring Boot 3.x using domain events, ApplicationEventPublisher, @TransactionalEventListener, and distributed messaging with Kafka and Spring Cloud Stream.

When to Use This Skill

Use this skill when building applications that require:

  • Loose coupling between microservices through event-based communication
  • Domain event publishing from aggregate roots in DDD architectures
  • Transactional event listeners ensuring consistency after database commits
  • Distributed messaging with Kafka for inter-service communication
  • Event streaming with Spring Cloud Stream for reactive systems
  • Reliability using the transactional outbox pattern
  • Asynchronous communication between bounded contexts
  • Event sourcing foundations with proper event sourcing patterns

Instructions

Follow these steps to implement event-driven architecture patterns in Spring Boot:

1. Design Domain Events

Create immutable event classes extending a base DomainEvent class. Include eventId, occurredAt, and correlationId fields for traceability.

2. Define Event Publishing

Add ApplicationEventPublisher to services that need to publish events. Publish events after domain state changes complete.

3. Configure Transactional Listeners

Use @TransactionalEventListener with phase = AFTER_COMMIT to ensure events are only processed after successful database transaction.

4. Set Up Kafka Infrastructure

Configure KafkaTemplate for publishing events to topics. Create @KafkaListener beans to consume events from other services.

5. Implement Spring Cloud Stream

Use functional programming model with Consumer bean definitions for reactive event consumption. Configure bindings in application.yml.

6. Handle Failure Scenarios

Implement retry logic with exponential backoff. Configure dead-letter queues for failed messages. Make event handlers idempotent.

7. Implement Outbox Pattern

Create OutboxEvent entity to store events atomically with business data. Use scheduled job to publish outbox events to message broker.

8. Add Observability

Enable Spring Cloud Sleuth for distributed tracing. Monitor event processing metrics through Actuator endpoints.

Setup and Configuration

Required Dependencies

To implement event-driven patterns, include these dependencies in your project:

Maven:

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- Kafka for distributed messaging -->
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>

    <!-- Spring Cloud Stream -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream</artifactId>
        <version>4.0.4</version> // Use latest compatible version
    </dependency>

    <!-- Testing -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Testcontainers for integration testing -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers</artifactId>
        <version>1.19.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Gradle:

dependencies {
    // Spring Boot Web
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // Spring Data JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // Kafka
    implementation 'org.springframework.kafka:spring-kafka'

    // Spring Cloud Stream
    implementation 'org.springframework.cloud:spring-cloud-stream:4.0.4'

    // Testing
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.testcontainers:testcontainers:1.19.0'
}

Basic Configuration

Configure your application for event-driven architecture:

# Server Configuration
server.port=8080

# Kafka Configuration
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer

# Spring Cloud Stream Configuration
spring.cloud.stream.kafka.binder.brokers=localhost:9092

Core Patterns

1. Domain Events Design

Create immutable domain events for business domain changes:

// Domain event base class
public abstract class DomainEvent {
    private final UUID eventId;
    private final LocalDateTime occurredAt;
    private final UUID correlationId;

    protected DomainEvent() {
        this.eventId = UUID.randomUUID();
        this.occurredAt = LocalDateTime.now();
        this.correlationId = UUID.randomUUID();
    }

    protected DomainEvent(UUID correlationId) {
        this.eventId = UUID.randomUUID();
        this.occurredAt = LocalDateTime.now();
        this.correlationId = correlationId;
    }

    // Getters
    public UUID getEventId() { return eventId; }
    public LocalDateTime getOccurredAt() { return occurredAt; }
    public UUID getCorrelationId() { return correlationId; }
}

// Specific domain events
public class ProductCreatedEvent extends DomainEvent {
    private final ProductId productId;
    private final String name;
    private final BigDecimal price;
    private final Integer stock;

    public ProductCreatedEvent(ProductId productId, String name, BigDecimal price, Integer stock) {
        super();
        this.productId = productId;
        this.name = name;
        this.price = price;
        this.stock = stock;
    }

    // Getters
    public ProductId getProductId() { return productId; }
    public String getName() { return name; }
    public BigDecimal getPrice() { return price; }
    public Integer getStock() { return stock; }
}

2. Aggregate Root with Event Publishing

Implement aggregates that publish domain events:

@Entity
@Getter
@ToString
@EqualsAndHashCode(of = "id")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
    @Id
    private ProductId id;
    private String name;
    private BigDecimal price;
    private Integer stock;

    @Transient
    private List<DomainEvent> domainEvents = new ArrayList<>();

    public static Product create(String name, BigDecimal price, Integer stock) {
        Product product = new Product();
        product.id = ProductId.generate();
        product.name = name;
        product.price = price;
        product.stock = stock;
        product.domainEvents.add(new ProductCreatedEvent(product.id, name, price, stock));
        return product;
    }

    public void decreaseStock(Integer quantity) {
        this.stock -= quantity;
        this.domainEvents.add(new ProductStockDecreasedEvent(this.id, quantity, this.stock));
    }

    public List<DomainEvent> getDomainEvents() {
        return new ArrayList<>(domainEvents);
    }

    public void clearDomainEvents() {
        domainEvents.clear();
    }
}

3. Application Event Publishing

Publish domain events from application services:

@Service
@RequiredArgsConstructor
@Transactional
public class ProductApplicationService {
    private final ProductRepository productRepository;
    private final ApplicationEventPublisher eventPublisher;

    public ProductResponse createProduct(CreateProductRequest request) {
        Product product = Product.create(
            request.getName(),
            request.getPrice(),
            request.getStock()
        );

        productRepository.save(product);

        // Publish domain events
        product.getDomainEvents().forEach(eventPublisher::publishEvent);
        product.clearDomainEvents();

        return mapToResponse(product);
    }
}

4. Local Event Handling

Handle events with transactional event listeners:

@Component
@RequiredArgsConstructor
public class ProductEventHandler {
    private final NotificationService notificationService;
    private final AuditService auditService;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onProductCreated(ProductCreatedEvent event) {
        auditService.logProductCreation(
            event.getProductId().getValue(),
            event.getName(),
            event.getPrice(),
            event.getCorrelationId()
        );

        notificationService.sendProductCreatedNotification(event.getName());
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onProductStockDecreased(ProductStockDecreasedEvent event) {
        notificationService.sendStockUpdateNotification(
            event.getProductId().getValue(),
            event.getQuantity()
        );
    }
}

5. Distributed Event Publishing

Publish events to Kafka for inter-service communication:

@Component
@RequiredArgsConstructor
public class ProductEventPublisher {
    private final KafkaTemplate<String, Object> kafkaTemplate;

    public void publishProductCreatedEvent(ProductCreatedEvent event) {
        ProductCreatedEventDto dto = mapToDto(event);
        kafkaTemplate.send("product-events", event.getProductId().getValue(), dto);
    }

    private ProductCreatedEventDto mapToDto(ProductCreatedEvent event) {
        return new ProductCreatedEventDto(
            event.getEventId(),
            event.getProductId().getValue(),
            event.getName(),
            event.getPrice(),
            event.getStock(),
            event.getOccurredAt(),
            event.getCorrelationId()
        );
    }
}

6. Event Consumer with Spring Cloud Stream

Consume events using functional programming style:

@Component
@RequiredArgsConstructor
public class ProductEventStreamConsumer {
    private final OrderService orderService;

    @Bean
    public Consumer<ProductCreatedEventDto> productCreatedConsumer() {
        return event -> {
            orderService.onProductCreated(event);
        };
    }

    @Bean
    public Consumer<ProductStockDecreasedEventDto> productStockDecreasedConsumer() {
        return event -> {
            orderService.onProductStockDecreased(event);
        };
    }
}

Advanced Patterns

Transactional Outbox Pattern

Ensure reliable event publishing with the outbox pattern:

@Entity
@Table(name = "outbox_events")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OutboxEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    private String aggregateId;
    private String eventType;
    private String payload;
    private UUID correlationId;
    private LocalDateTime createdAt;
    private LocalDateTime publishedAt;
    private Integer retryCount;
}

@Component
@RequiredArgsConstructor
public class OutboxEventProcessor {
    private final OutboxEventRepository outboxRepository;
    private final KafkaTemplate<String, Object> kafkaTemplate;

    @Scheduled(fixedDelay = 5000)
    @Transactional
    public void processPendingEvents() {
        List<OutboxEvent> pendingEvents = outboxRepository.findByPublishedAtNull();

        for (OutboxEvent event : pendingEvents) {
            try {
                kafkaTemplate.send("product-events", event.getAggregateId(), event.getPayload());
                event.setPublishedAt(LocalDateTime.now());
                outboxRepository.save(event);
            } catch (Exception e) {
                event.setRetryCount(event.getRetryCount() + 1);
                outboxRepository.save(event);
            }
        }
    }
}

Testing Strategies

Unit Testing Domain Events

Test domain event publishing and handling:

class ProductTest {
    @Test
    void shouldPublishProductCreatedEventOnCreation() {
        Product product = Product.create("Test Product", BigDecimal.TEN, 100);

        assertThat(product.getDomainEvents()).hasSize(1);
        assertThat(product.getDomainEvents().get(0))
            .isInstanceOf(ProductCreatedEvent.class);
    }
}

@ExtendWith(MockitoExtension.class)
class ProductEventHandlerTest {
    @Mock
    private NotificationService notificationService;

    @InjectMocks
    private ProductEventHandler handler;

    @Test
    void shouldHandleProductCreatedEvent() {
        ProductCreatedEvent event = new ProductCreatedEvent(
            ProductId.of("123"), "Product", BigDecimal.TEN, 100
        );

        handler.onProductCreated(event);

        verify(notificationService).sendProductCreatedNotification("Product");
    }
}

Integration Testing with Testcontainers

Test Kafka integration with Testcontainers:

@SpringBootTest
@Testcontainers
class KafkaEventIntegrationTest {
    @Container
    static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));

    @Autowired
    private ProductApplicationService productService;

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }

    @Test
    void shouldPublishEventToKafka() {
        CreateProductRequest request = new CreateProductRequest(
            "Test Product", BigDecimal.valueOf(99.99), 50
        );

        ProductResponse response = productService.createProduct(request);

        // Verify event was published
        verify(eventPublisher).publishProductCreatedEvent(any(ProductCreatedEvent.class));
    }
}

Best Practices

Event Design Guidelines

  • Use past tense naming: ProductCreated, not CreateProduct
  • Keep events immutable: All fields should be final
  • Include correlation IDs: For tracing events across services
  • Serialize to JSON: For cross-service compatibility

Transactional Consistency

  • Use AFTER_COMMIT phase: Ensures events are published after successful database transaction
  • Implement idempotent handlers: Handle duplicate events gracefully
  • Add retry mechanisms: For failed event processing

Error Handling

  • Implement dead-letter queues: For events that fail processing
  • Log all failures: Include sufficient context for debugging
  • Set appropriate timeouts: For event processing operations

Performance Considerations

  • Batch event processing: When handling high volumes
  • Use proper partitioning: For Kafka topics
  • Monitor event latencies: Set up alerts for slow processing

Examples and References

See the following resources for comprehensive examples:

Troubleshooting

Common Issues

Events not being published:

  • Check transaction phase configuration
  • Verify ApplicationEventPublisher is properly autowired
  • Ensure transaction is committed before event publishing

Kafka connection issues:

  • Verify bootstrap servers configuration
  • Check network connectivity to Kafka
  • Ensure proper serialization configuration

Event handling failures:

  • Check for circular dependencies in event handlers
  • Verify transaction boundaries
  • Monitor for exceptions in event processing

Debug Tips

  • Enable debug logging for Spring events: logging.level.org.springframework.context=DEBUG
  • Use correlation IDs to trace events across services
  • Monitor event processing metrics in Actuator endpoints

Constraints and Warnings

  • Events published with @TransactionalEventListener only fire after transaction commit; ensure this matches your consistency requirements.
  • Avoid publishing large objects in events as this can cause memory pressure and serialization issues.
  • Be cautious with async event handlers as they execute in separate threads and may cause concurrency issues.
  • Kafka consumers must handle duplicate messages by implementing idempotent processing.
  • Event ordering is not guaranteed in distributed systems; design handlers to be order-independent.
  • Never perform blocking operations in event listeners that run on the main transaction thread.
  • Monitor for event processing backlogs as they can indicate system capacity issues.

Examples

Input: Monolithic Order Processing (Anti-Pattern)

@Service
public class OrderService {
    @Transactional
    public Order processOrder(OrderRequest request) {
        Order order = orderRepository.save(request);
        inventoryService.reserve(order.getItems());
        paymentService.charge(order.getPayment());
        shippingService.schedule(order);
        emailService.sendConfirmation(order);
        return order;
    }
}

Output: Event-Driven Order Processing

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public Order processOrder(OrderRequest request) {
        Order order = Order.create(request);
        orderRepository.save(order);

        // Publish event after transaction commits
        eventPublisher.publishEvent(new OrderCreatedEvent(
            order.getId(),
            order.getItems(),
            order.getPayment()
        ));

        return order;
    }
}

@Component
public class OrderEventHandler {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderCreated(OrderCreatedEvent event) {
        // These execute asynchronously after the order is saved
        inventoryService.reserve(event.getItems());
        paymentService.charge(event.getPayment());
    }
}

Input: Synchronous External Service Call

@Service
public class NotificationService {
    public void sendOrderNotification(Order order) {
        emailClient.send(order); // Blocking call
    }
}

Output: Asynchronous Event-Driven Notification

public class OrderCreatedEvent extends DomainEvent {
    private final OrderId orderId;
    private final String customerEmail;
    private final BigDecimal total;

    // Constructor and getters
}

@Component
public class NotificationEventHandler {
    @KafkaListener(topics = "order-events")
    public void handleOrderEvent(OrderCreatedEventDto event) {
        // Process asynchronously without blocking the order flow
        emailService.sendOrderConfirmation(event);
    }
}

Input: Event without Traceability

eventPublisher.publishEvent(new ProductCreatedEvent(productId, name));

Output: Event with Full Traceability

public class ProductCreatedEvent extends DomainEvent {
    private final EventId eventId = EventId.generate();
    private final ProductId productId;
    private final String name;
    private final Instant occurredAt = Instant.now();
    private final CorrelationId correlationId = CorrelationId.generate();

    // Includes metadata for distributed tracing
    public Map<String, String> getMetadata() {
        return Map.of(
            "eventId", eventId.toString(),
            "correlationId", correlationId.toString(),
            "timestamp", occurredAt.toString()
        );
    }
}