Files
gh-giuseppe-trisciuoglio-de…/skills/spring-boot/spring-boot-event-driven-patterns/references/event-driven-patterns-reference.md
2025-11-29 18:28:30 +08:00

12 KiB

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).

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

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.

@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.

@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

public interface ApplicationEventPublisher {
    void publishEvent(ApplicationEvent event);
    void publishEvent(Object event);  // Modern approach
}

Usage Pattern

@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

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

@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

@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

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

// 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

<!-- 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

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

// 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

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)

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

@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

@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

@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

# 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

@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();
    }
}
  • 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

Patterns and Best Practices