Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:30 +08:00
commit 171acedaa4
220 changed files with 85967 additions and 0 deletions

View File

@@ -0,0 +1,520 @@
---
name: spring-boot-event-driven-patterns
description: Implement Event-Driven Architecture (EDA) in Spring Boot using ApplicationEvent, @EventListener, and Kafka. Use for building loosely-coupled microservices with domain events, transactional event listeners, and distributed messaging patterns.
allowed-tools: Read, Write, Bash
category: backend
tags: [spring-boot, java, event-driven, eda, kafka, messaging, domain-events, microservices, spring-cloud-stream]
version: 1.1.0
---
# 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
## Setup and Configuration
### Required Dependencies
To implement event-driven patterns, include these dependencies in your project:
**Maven:**
```xml
<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:**
```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:
```properties
# 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:
```java
// 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:
```java
@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:
```java
@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:
```java
@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:
```java
@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:
```java
@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:
```java
@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:
```java
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:
```java
@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:
- [Complete working examples](references/examples.md)
- [Detailed implementation patterns](references/event-driven-patterns-reference.md)
## 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
---
This skill provides the essential patterns and best practices for implementing event-driven architectures in Spring Boot applications.

View File

@@ -0,0 +1,485 @@
# Spring Boot Event-Driven Patterns - References
Complete API reference for event-driven architecture in Spring Boot applications.
## Domain Event Annotations and Interfaces
### ApplicationEvent
Base class for Spring events (deprecated in newer versions in favor of plain objects).
```java
public abstract class ApplicationEvent extends EventObject {
private final long timestamp;
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
}
public long getTimestamp() {
return timestamp;
}
}
// Modern approach: Use plain POJOs
public record ProductCreatedEvent(String productId, String name, BigDecimal price) {}
```
### Custom Domain Event Base Class
```java
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();
}
}
```
## Event Publishing Annotations
### @EventListener
Register event listener methods.
```java
@EventListener
public void onProductCreated(ProductCreatedEvent event) { }
@EventListener(condition = "#event.productId == '123'") // SpEL condition
public void onSpecificProduct(ProductCreatedEvent event) { }
@EventListener(classes = { ProductCreatedEvent.class, ProductUpdatedEvent.class })
public void onProductEvent(DomainEvent event) { }
```
### @TransactionalEventListener
Listen to events within transaction lifecycle.
```java
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onProductCreated(ProductCreatedEvent event) { }
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void beforeCommit(ProductCreatedEvent event) { }
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void afterRollback(ProductCreatedEvent event) { }
```
**TransactionPhase Values:**
- `BEFORE_COMMIT` - Before transaction commits
- `AFTER_COMMIT` - After successful commit (recommended)
- `AFTER_ROLLBACK` - After transaction rollback
- `AFTER_COMPLETION` - After transaction completion (success or rollback)
## Event Publishing Reference
### ApplicationEventPublisher Interface
```java
public interface ApplicationEventPublisher {
void publishEvent(ApplicationEvent event);
void publishEvent(Object event); // Modern approach
}
```
### Usage Pattern
```java
@Service
@RequiredArgsConstructor
public class ProductService {
private final ApplicationEventPublisher eventPublisher;
public Product create(CreateProductRequest request) {
Product product = Product.create(request);
Product saved = repository.save(product);
// Publish events
saved.getDomainEvents().forEach(eventPublisher::publishEvent);
saved.clearDomainEvents();
return saved;
}
}
```
## Kafka Spring Cloud Stream Reference
### Stream Binders Configuration
```yaml
spring:
cloud:
stream:
kafka:
binder:
brokers: localhost:9092
default-binder: kafka
configuration:
linger.ms: 10
batch.size: 1024
bindings:
# Consumer binding
productCreatedConsumer-in-0:
destination: product-events
group: product-service
consumer:
max-attempts: 3
back-off-initial-interval: 1000
back-off-max-interval: 10000
# Producer binding
eventPublisher-out-0:
destination: product-events
producer:
partition-key-expression: headers['partitionKey']
```
### Consumer Function Binding
```java
@Configuration
public class EventConsumers {
@Bean
public java.util.function.Consumer<ProductCreatedEvent> productCreatedConsumer(
InventoryService inventoryService) {
return event -> {
log.info("Consumed: {}", event);
inventoryService.process(event);
};
}
// Multiple consumers
@Bean
public java.util.function.Consumer<ProductUpdatedEvent> productUpdatedConsumer() {
return event -> { };
}
}
// application.yml
spring.cloud.stream.bindings.productCreatedConsumer-in-0.destination=product-events
spring.cloud.stream.bindings.productUpdatedConsumer-in-0.destination=product-events
```
### Producer Function Binding
```java
@Configuration
public class EventProducers {
@Bean
public java.util.function.Supplier<ProductCreatedEvent> eventPublisher() {
return () -> new ProductCreatedEvent("prod-123", "Laptop", BigDecimal.TEN);
}
}
// application.yml
spring.cloud.stream.bindings.eventPublisher-out-0.destination=product-events
```
## Transactional Outbox Pattern Reference
### Outbox Entity Schema
```sql
CREATE TABLE outbox_events (
id UUID PRIMARY KEY,
aggregate_id VARCHAR(255) NOT NULL,
aggregate_type VARCHAR(255),
event_type VARCHAR(255) NOT NULL,
payload TEXT NOT NULL,
correlation_id UUID,
created_at TIMESTAMP NOT NULL,
published_at TIMESTAMP,
retry_count INTEGER DEFAULT 0,
KEY idx_published (published_at),
KEY idx_created (created_at)
);
```
### Implementation Pattern
```java
// In single transaction:
// 1. Update aggregate
product = repository.save(product);
// 2. Store events in outbox
product.getDomainEvents().forEach(event -> {
outboxRepository.save(new OutboxEvent(
aggregateId, eventType, payload, correlationId
));
});
// Then separately, scheduled task publishes from outbox
@Scheduled(fixedDelay = 5000)
public void publishPendingEvents() {
List<OutboxEvent> pending = outboxRepository.findByPublishedAtIsNull();
pending.forEach(event -> {
kafkaTemplate.send(topic, event.getPayload());
event.setPublishedAt(now());
});
}
```
## Maven Dependencies
```xml
<!-- Local Events (Spring Framework core) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!-- Kafka -->
<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>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
<version>4.0.4</version>
</dependency>
<!-- Jackson for JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
```
## Gradle Dependencies
```gradle
dependencies {
// Local Events
implementation 'org.springframework:spring-context'
// Kafka
implementation 'org.springframework.kafka:spring-kafka'
// Spring Cloud Stream
implementation 'org.springframework.cloud:spring-cloud-stream:4.0.4'
implementation 'org.springframework.cloud:spring-cloud-stream-binder-kafka:4.0.4'
// Jackson
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
}
```
## Event Ordering Guarantees
### Kafka Partition Key Strategy
```java
// Events with same product must be in same partition
kafkaTemplate.send(topic,
productId, // Key: ensures ordering per product
event); // Value
// Consumer configuration
spring.kafka.consumer.properties.isolation.level=read_committed
spring.cloud.stream.kafka.binder.configuration.isolation.level=read_committed
```
## Error Handling Patterns
### Retry with Backoff
```yaml
spring:
cloud:
stream:
bindings:
eventConsumer-in-0:
consumer:
max-attempts: 3
back-off-initial-interval: 1000 # 1 second
back-off-max-interval: 10000 # 10 seconds
back-off-multiplier: 2.0 # Exponential
default-retryable: true
retryable-exceptions:
com.example.RetryableException: true
```
### Dead Letter Topic (DLT)
```yaml
spring:
cloud:
stream:
kafka:
bindings:
eventConsumer-in-0:
consumer:
enable-dlq: true
dlq-name: product-events.dlq
dlq-producer-properties:
linger.ms: 5
```
## Idempotency Patterns
### Idempotent Consumer
```java
@Component
public class IdempotentEventHandler {
private final IdempotencyKeyRepository idempotencyRepository;
private final EventProcessingService eventService;
@EventListener
public void handle(DomainEvent event) throws Exception {
String idempotencyKey = event.getEventId().toString();
// Check if already processed
if (idempotencyRepository.exists(idempotencyKey)) {
log.info("Event already processed: {}", idempotencyKey);
return;
}
try {
// Process event
eventService.process(event);
// Mark as processed
idempotencyRepository.save(new IdempotencyKey(idempotencyKey));
} catch (Exception e) {
log.error("Event processing failed: {}", idempotencyKey, e);
throw e;
}
}
}
```
## Testing Event-Driven Systems
### Local Event Testing
```java
@SpringBootTest
class EventDrivenTest {
@Autowired
private ApplicationEventPublisher eventPublisher;
@MockBean
private EventHandler handler;
@Test
void shouldHandleEvent() {
// Arrange
ProductCreatedEvent event = new ProductCreatedEvent("123", "Laptop", BigDecimal.TEN);
// Act
eventPublisher.publishEvent(event);
// Assert
verify(handler).onProductCreated(event);
}
}
```
### Kafka Testing with Testcontainers
```java
@SpringBootTest
@Testcontainers
class KafkaEventTest {
@Container
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));
@DynamicPropertySource
static void setupProperties(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
}
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@Test
void shouldPublishEventToKafka() throws Exception {
ProductCreatedEvent event = new ProductCreatedEvent("123", "Laptop", BigDecimal.TEN);
kafkaTemplate.send("product-events", "123", event).get(5, TimeUnit.SECONDS);
// Verify consumption
}
}
```
## Monitoring and Observability
### Spring Boot Actuator Metrics
```properties
# Enable metrics
management.endpoints.web.exposure.include=metrics,health
# Kafka metrics
kafka.controller.metrics.topic.under_replication_count
kafka.log.leader_election.latency.avg
```
### Custom Event Metrics
```java
@Component
@RequiredArgsConstructor
public class EventMetrics {
private final MeterRegistry meterRegistry;
public void recordEventPublished(String eventType) {
meterRegistry.counter("events.published", "type", eventType).increment();
}
public void recordEventProcessed(String eventType, long durationMs) {
meterRegistry.timer("events.processed", "type", eventType).record(durationMs, TimeUnit.MILLISECONDS);
}
public void recordEventFailed(String eventType) {
meterRegistry.counter("events.failed", "type", eventType).increment();
}
}
```
## Related Skills
- **spring-boot-crud-patterns** - Domain events in CRUD operations
- **spring-boot-rest-api-standards** - Event notifications via webhooks
- **spring-boot-test-patterns** - Testing event-driven systems
- **spring-boot-dependency-injection** - Dependency injection in event handlers
## External Resources
### Official Documentation
- [Spring ApplicationContext](https://docs.spring.io/spring-framework/reference/core/beans/context-introduction.html)
- [Spring Cloud Stream](https://spring.io/projects/spring-cloud-stream)
- [Kafka Spring Integration](https://docs.spring.io/spring-kafka/docs/current/reference/html/)
### Patterns and Best Practices
- [Event Sourcing Pattern](https://martinfowler.com/eaaDev/EventSourcing.html)
- [Saga Pattern](https://microservices.io/patterns/data/saga.html)
- [Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html)

View File

@@ -0,0 +1,510 @@
# Spring Boot Event-Driven Patterns - Examples
Comprehensive examples demonstrating event-driven architecture from basic local events to advanced distributed messaging.
## Example 1: Basic Domain Events
A simple product lifecycle with domain events.
```java
// Domain event
public class ProductCreatedEvent extends DomainEvent {
private final String productId;
private final String name;
private final BigDecimal price;
public ProductCreatedEvent(String productId, String name, BigDecimal price) {
super();
this.productId = productId;
this.name = name;
this.price = price;
}
// Getters
}
// Aggregate publishing events
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
private String id;
private String name;
private BigDecimal price;
@Transient
private List<DomainEvent> domainEvents = new ArrayList<>();
public static Product create(String name, BigDecimal price) {
Product product = new Product();
product.id = UUID.randomUUID().toString();
product.name = name;
product.price = price;
// Publish domain event
product.publishEvent(new ProductCreatedEvent(product.id, name, price));
return product;
}
protected void publishEvent(DomainEvent event) {
domainEvents.add(event);
}
public List<DomainEvent> getDomainEvents() {
return new ArrayList<>(domainEvents);
}
public void clearDomainEvents() {
domainEvents.clear();
}
}
```
---
## Example 2: Local Event Publishing
Using ApplicationEventPublisher for in-process events.
```java
// Application service
@Service
@Slf4j
@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());
Product saved = productRepository.save(product);
// Publish domain events
saved.getDomainEvents().forEach(event -> {
log.debug("Publishing event: {}", event.getClass().getSimpleName());
eventPublisher.publishEvent(event);
});
saved.clearDomainEvents();
return mapper.toResponse(saved);
}
}
// Event listener
@Component
@Slf4j
@RequiredArgsConstructor
public class ProductEventHandler {
private final NotificationService notificationService;
private final InventoryService inventoryService;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onProductCreated(ProductCreatedEvent event) {
log.info("Handling ProductCreatedEvent");
// Send notification
notificationService.sendProductCreatedNotification(
event.getName(), event.getPrice()
);
// Update inventory
inventoryService.registerProduct(event.getProductId());
}
}
// Test
@SpringBootTest
class ProductEventTest {
@Autowired
private ProductApplicationService productService;
@MockBean
private NotificationService notificationService;
@Autowired
private ProductRepository productRepository;
@Test
void shouldPublishProductCreatedEvent() {
// Act
productService.createProduct(
new CreateProductRequest("Laptop", BigDecimal.valueOf(999.99))
);
// Assert - Event was handled
verify(notificationService).sendProductCreatedNotification(
"Laptop", BigDecimal.valueOf(999.99)
);
}
}
```
---
## Example 3: Transactional Outbox Pattern
Ensures reliable event publishing even on failures.
```java
// Outbox entity
@Entity
@Table(name = "outbox_events")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OutboxEvent {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String aggregateId;
private String eventType;
@Column(columnDefinition = "TEXT")
private String payload;
private LocalDateTime createdAt;
private LocalDateTime publishedAt;
private Integer retryCount;
}
// Application service using outbox
@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class ProductApplicationService {
private final ProductRepository productRepository;
private final OutboxEventRepository outboxRepository;
private final ObjectMapper objectMapper;
public ProductResponse createProduct(CreateProductRequest request) {
Product product = Product.create(request.getName(), request.getPrice());
Product saved = productRepository.save(product);
// Store event in outbox (same transaction)
saved.getDomainEvents().forEach(event -> {
try {
String payload = objectMapper.writeValueAsString(event);
OutboxEvent outboxEvent = OutboxEvent.builder()
.aggregateId(saved.getId())
.eventType(event.getClass().getSimpleName())
.payload(payload)
.createdAt(LocalDateTime.now())
.retryCount(0)
.build();
outboxRepository.save(outboxEvent);
log.debug("Outbox event created: {}", event.getClass().getSimpleName());
} catch (Exception e) {
log.error("Failed to create outbox event", e);
throw new RuntimeException(e);
}
});
return mapper.toResponse(saved);
}
}
// Scheduled publisher
@Component
@Slf4j
@RequiredArgsConstructor
public class OutboxEventPublisher {
private final OutboxEventRepository outboxRepository;
private final KafkaTemplate<String, String> kafkaTemplate;
private final ObjectMapper objectMapper;
@Scheduled(fixedDelay = 5000)
@Transactional
public void publishPendingEvents() {
List<OutboxEvent> pending = outboxRepository.findByPublishedAtIsNull();
for (OutboxEvent event : pending) {
try {
kafkaTemplate.send("product-events",
event.getAggregateId(), event.getPayload());
event.setPublishedAt(LocalDateTime.now());
outboxRepository.save(event);
log.info("Published outbox event: {}", event.getId());
} catch (Exception e) {
log.error("Failed to publish event: {}", event.getId(), e);
event.setRetryCount(event.getRetryCount() + 1);
outboxRepository.save(event);
}
}
}
}
```
---
## Example 4: Kafka Event Publishing
Distributed event publishing with Spring Cloud Stream.
```java
// Application configuration
@Configuration
public class KafkaConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}
// Event publisher
@Component
@Slf4j
@RequiredArgsConstructor
public class KafkaProductEventPublisher {
private final KafkaTemplate<String, Object> kafkaTemplate;
public void publishProductCreatedEvent(ProductCreatedEvent event) {
log.info("Publishing ProductCreatedEvent to Kafka: {}", event.getProductId());
kafkaTemplate.send("product-events",
event.getProductId(),
event);
}
}
// Event consumer
@Component
@Slf4j
@RequiredArgsConstructor
public class ProductEventStreamConsumer {
private final InventoryService inventoryService;
@Bean
public java.util.function.Consumer<ProductCreatedEvent> productCreatedConsumer() {
return event -> {
log.info("Consumed ProductCreatedEvent: {}", event.getProductId());
inventoryService.registerProduct(event.getProductId(), event.getName());
};
}
@Bean
public java.util.function.Consumer<ProductUpdatedEvent> productUpdatedConsumer() {
return event -> {
log.info("Consumed ProductUpdatedEvent: {}", event.getProductId());
inventoryService.updateProduct(event.getProductId(), event.getPrice());
};
}
}
// Application properties
```
**application.yml:**
```yaml
spring:
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
consumer:
group-id: product-service
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
cloud:
stream:
bindings:
productCreatedConsumer-in-0:
destination: product-events
group: product-inventory-service
productUpdatedConsumer-in-0:
destination: product-events
group: product-inventory-service
```
---
## Example 5: Event Saga Pattern
Coordinating multiple services with events.
```java
// Events
public class OrderPlacedEvent extends DomainEvent {
private final String orderId;
private final String productId;
private final Integer quantity;
// ...
}
public class OrderPaymentConfirmedEvent extends DomainEvent {
private final String orderId;
// ...
}
// Saga orchestrator
@Component
@Slf4j
@RequiredArgsConstructor
public class OrderFulfillmentSaga {
private final OrderService orderService;
private final PaymentService paymentService;
private final InventoryService inventoryService;
private final ApplicationEventPublisher eventPublisher;
@Transactional
@EventListener
public void onOrderPlaced(OrderPlacedEvent event) {
log.info("Starting order fulfillment saga for order: {}", event.getOrderId());
try {
// Step 1: Reserve inventory
inventoryService.reserveStock(event.getProductId(), event.getQuantity());
log.info("Inventory reserved for order: {}", event.getOrderId());
// Step 2: Process payment
paymentService.processPayment(event.getOrderId());
log.info("Payment processed for order: {}", event.getOrderId());
// Step 3: Publish confirmation
eventPublisher.publishEvent(new OrderPaymentConfirmedEvent(event.getOrderId()));
// Step 4: Update order status
orderService.markAsConfirmed(event.getOrderId());
log.info("Order confirmed: {}", event.getOrderId());
} catch (PaymentFailedException e) {
log.warn("Payment failed, releasing inventory");
inventoryService.releaseStock(event.getProductId(), event.getQuantity());
orderService.markAsFailed(event.getOrderId(), e.getMessage());
}
}
}
// Test
@SpringBootTest
class OrderFulfillmentSagaTest {
@Autowired
private ApplicationEventPublisher eventPublisher;
@MockBean
private InventoryService inventoryService;
@MockBean
private PaymentService paymentService;
@MockBean
private OrderService orderService;
@Test
void shouldCompleteOrderFulfillmentSaga() {
// Arrange
OrderPlacedEvent event = new OrderPlacedEvent("order-123", "product-456", 2);
// Act
eventPublisher.publishEvent(event);
// Assert
verify(inventoryService).reserveStock("product-456", 2);
verify(paymentService).processPayment("order-123");
verify(orderService).markAsConfirmed("order-123");
}
}
```
---
## Example 6: Event Sourcing Foundation
Storing state changes as events.
```java
// Event store
@Repository
public interface EventStoreRepository extends JpaRepository<StoredEvent, UUID> {
List<StoredEvent> findByAggregateIdOrderBySequenceAsc(String aggregateId);
}
// Stored event
@Entity
@Table(name = "event_store")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StoredEvent {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String aggregateId;
private String eventType;
private Integer sequence;
@Column(columnDefinition = "TEXT")
private String payload;
private LocalDateTime occurredAt;
}
// Event sourcing service
@Service
@Slf4j
@RequiredArgsConstructor
public class EventSourcingService {
private final EventStoreRepository eventStoreRepository;
private final ObjectMapper objectMapper;
@Transactional
public void storeEvent(String aggregateId, DomainEvent event) {
try {
List<StoredEvent> existing = eventStoreRepository
.findByAggregateIdOrderBySequenceAsc(aggregateId);
Integer nextSequence = existing.isEmpty() ? 1 :
existing.get(existing.size() - 1).getSequence() + 1;
StoredEvent storedEvent = StoredEvent.builder()
.aggregateId(aggregateId)
.eventType(event.getClass().getSimpleName())
.sequence(nextSequence)
.payload(objectMapper.writeValueAsString(event))
.occurredAt(LocalDateTime.now())
.build();
eventStoreRepository.save(storedEvent);
log.info("Event stored: {} for aggregate: {}",
event.getClass().getSimpleName(), aggregateId);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to store event", e);
}
}
public List<DomainEvent> getEventHistory(String aggregateId) {
return eventStoreRepository
.findByAggregateIdOrderBySequenceAsc(aggregateId)
.stream()
.map(this::deserializeEvent)
.collect(Collectors.toList());
}
private DomainEvent deserializeEvent(StoredEvent stored) {
try {
Class<?> eventClass = Class.forName(
"com.example.product.domain.event." + stored.getEventType());
return (DomainEvent) objectMapper.readValue(stored.getPayload(), eventClass);
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize event", e);
}
}
}
```
These examples cover local events, transactional outbox pattern, Kafka publishing, saga coordination, and event sourcing foundations for comprehensive event-driven architecture.