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
-
Implementing event-driven microservices with Kafka messaging
-
Publishing domain events from aggregate roots in DDD architectures
-
Setting up transactional event listeners that fire after database commits
-
Adding async messaging with producers and consumers via Spring Kafka
-
Ensuring reliable event delivery using the transactional outbox pattern
-
Replacing synchronous calls with event-based communication between services
Quick Reference
Concept Description
Domain Events Immutable events extending DomainEvent base class with eventId, occurredAt, correlationId
Event Publishing ApplicationEventPublisher.publishEvent() for local, KafkaTemplate for distributed
Event Listening @TransactionalEventListener(phase = AFTER_COMMIT) for reliable handling
Kafka @KafkaListener(topics = "...") for distributed event consumption
Spring Cloud Stream Functional programming model with Consumer beans
Outbox Pattern Atomic event storage with business data, scheduled publisher
Examples
Monolithic to Event-Driven Refactoring
Before (Anti-Pattern):
@Transactional public Order processOrder(OrderRequest request) { Order order = orderRepository.save(request); inventoryService.reserve(order.getItems()); // Blocking paymentService.charge(order.getPayment()); // Blocking emailService.sendConfirmation(order); // Blocking return order; }
After (Event-Driven):
@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()));
return order;
}
@Component public class OrderEventHandler { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleOrderCreated(OrderCreatedEvent event) { // Execute asynchronously after the order is saved inventoryService.reserve(event.getItems()); paymentService.charge(event.getPayment()); } }
See examples.md for complete working examples.
Instructions
- Design Domain Events
Create immutable event classes extending a base DomainEvent class:
public abstract class DomainEvent { private final UUID eventId; private final LocalDateTime occurredAt; private final UUID correlationId; }
public class ProductCreatedEvent extends DomainEvent { private final ProductId productId; private final String name; private final BigDecimal price; }
See domain-events-design.md for patterns.
- Publish Events from Aggregates
Add domain events to aggregate roots, publish via ApplicationEventPublisher :
@Service @Transactional public class ProductService { public Product createProduct(CreateProductRequest request) { Product product = Product.create(request.getName(), request.getPrice(), request.getStock()); repository.save(product);
product.getDomainEvents().forEach(eventPublisher::publishEvent);
product.clearDomainEvents();
return product;
}
}
See aggregate-root-patterns.md for DDD patterns.
- Handle Events Transactionally
Use @TransactionalEventListener for reliable event handling:
@Component public class ProductEventHandler { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onProductCreated(ProductCreatedEvent event) { notificationService.sendProductCreatedNotification(event.getName()); } }
Validate: Confirm the event handler fires only after the transaction commits by checking that the database state is committed before the handler executes.
See event-handling.md for handling patterns.
- Configure Kafka Infrastructure
Configure KafkaTemplate for publishing, @KafkaListener for consuming:
spring: kafka: bootstrap-servers: localhost:9092 producer: value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
Validate: Send a test event via KafkaTemplate and confirm it appears in the consumer logs before proceeding to production patterns.
See dependency-setup.md and configuration.md.
- Implement Outbox Pattern
Create OutboxEvent entity for atomic event storage:
@Entity public class OutboxEvent { private UUID id; private String aggregateId; private String eventType; private String payload; private LocalDateTime publishedAt; }
Validate: Confirm the scheduled processor picks up pending events by checking the publishedAt timestamp is set after the scheduled run.
Scheduled processor publishes pending events. See outbox-pattern.md.
- Handle Failure Scenarios
Implement retry logic, dead-letter queues, idempotent handlers:
@RetryableTopic(attempts = "3") @KafkaListener(topics = "product-events") public void handleProductEvent(ProductCreatedEventDto event) { orderService.onProductCreated(event); }
Validate: Confirm messages reach the dead-letter topic after exhausting retries before moving to observability.
- Add Observability
Enable Spring Cloud Sleuth for distributed tracing, monitor metrics.
Best Practices
-
Use past tense naming: ProductCreated (not CreateProduct )
-
Keep events immutable: All fields should be final
-
Include correlation IDs: For tracing events across services
-
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 with exponential backoff
-
Implement dead-letter queues: For events that fail processing after retries
-
Log all failures: Include sufficient context for debugging
-
Make handlers order-independent: Event ordering is not guaranteed in distributed systems
-
Batch event processing: When handling high volumes
-
Monitor event latencies: Set up alerts for slow processing
References
-
dependency-setup.md — Maven/Gradle dependencies
-
configuration.md — Kafka and Spring Cloud Stream configuration
-
domain-events-design.md — Domain event design patterns
-
aggregate-root-patterns.md — Aggregate root with event publishing
-
event-publishing.md — Local and distributed event publishing
-
event-handling.md — Event handling and consumption patterns
-
outbox-pattern.md — Transactional outbox pattern for reliability
-
testing-strategies.md — Unit and integration testing approaches
-
examples.md — Complete working examples
-
event-driven-patterns-reference.md — Detailed reference documentation
Constraints and Warnings
-
Events published with @TransactionalEventListener only fire after transaction commit
-
Avoid publishing large objects in events (memory pressure, serialization issues)
-
Be cautious with async event handlers (separate threads, concurrency issues)
-
Kafka consumers must handle duplicate messages (implement idempotent processing)
-
Event ordering is not guaranteed in distributed systems (design handlers to be order-independent)
-
Never perform blocking operations in event listeners on the main transaction thread
-
Monitor for event processing backlogs (indicate system capacity issues)
Related Skills
-
spring-boot-security-jwt — JWT authentication for secure event publishing
-
spring-boot-test-patterns — Testing event-driven applications
-
aws-sdk-java-v2-lambda — Event-driven processing with AWS Lambda
-
langchain4j-tool-function-calling-patterns — AI-driven event processing