Productivity

spring-boot-event-driven-patterns

giuseppe-trisciuoglio/developer-kit · updated Apr 8, 2026

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-boot-event-driven-patterns
summary

Event-Driven Architecture patterns for Spring Boot using domain events, transactional listeners, and Kafka messaging.

  • Covers domain event design, ApplicationEventPublisher integration, and @TransactionalEventListener configuration for local event handling with AFTER_COMMIT phase guarantees
  • Supports distributed messaging via Kafka and Spring Cloud Stream for inter-service communication with functional consumer beans
  • Implements the transactional outbox pattern for reliable event publis
skill.md

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

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

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

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

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

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

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

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

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