Back to Architecture & Design Patterns

spring-boot-event-driven-patterns

Spring BootEvent-Driven ArchitectureEDAKafkaMicroservicesApplicationEventDomain EventsSpring Cloud Stream
282📄 MIT🕒 2026-06-15Source ↗

Install this skill

npx skills add giuseppe-trisciuoglio/developer-kit

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

This skill focuses on implementing event-driven architectures within Spring Boot applications. It covers the mechanical integration of the application event system with distributed message brokers like Kafka. By emphasizing domain-driven design, it provides templates for creating immutable events and managing state transitions through aggregates. The patterns defined ensure that microservices remain decoupled while maintaining consistency across bounded contexts. You will learn to handle asynchronous communication, implement transactional outboxes to prevent data loss, and verify event flows with testing utilities. By combining standard Spring event propagation with cloud-stream abstractions, the skill facilitates building responsive systems that handle state updates reactively without blocking primary business logic execution. It prioritizes data integrity and operational visibility through correlation IDs and structured event publishing cycles.

When to Use This Skill

  • Decoupling order processing services from inventory updates
  • Synchronizing read models in a CQRS environment
  • Notifying external microservices after aggregate state changes
  • Auditing business-critical modifications via an immutable event stream

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • Setup Kafka events in my Spring Boot application
  • Implement domain events using Spring ApplicationEventPublisher
  • How do I use @TransactionalEventListener for database consistency
  • Configure Spring Cloud Stream with a Kafka binder
  • Pattern for publishing events from a JPA aggregate

Pro Tips

  • 💡Always define clear, immutable domain events that represent facts about past occurrences in your business domain.
  • 💡Combine `@TransactionalEventListener` with the Outbox Pattern to achieve robust transactional consistency when publishing distributed events from Spring Boot applications.
  • 💡When integrating with Kafka, consider using a strong schema registry and schema evolution strategies (e.g., Avro, Protobuf) to ensure backward and forward compatibility of your event messages.

What this skill does

  • Definition of immutable domain events for bounded contexts
  • Integration of Kafka with Spring Cloud Stream for asynchronous messaging
  • Transactional event listening to ensure atomicity with database updates
  • Pattern implementation for the transactional outbox to guarantee delivery
  • Application of DDD principles for aggregate-based event triggering

When not to use it

  • Simple monoliths where direct method calls suffice
  • Systems requiring strict ACID compliance across distributed nodes without compensations

Example workflow

  1. Define an immutable event class extending the base domain event
  2. Inject ApplicationEventPublisher into the domain service layer
  3. Publish the event after successfully persisting the aggregate
  4. Annotate a service method with @TransactionalEventListener to process the logic
  5. Configure a Kafka sink to propagate events across microservice boundaries

Prerequisites

  • Basic knowledge of Spring Boot and JPA
  • Understanding of core message queue concepts
  • Familiarity with DDD aggregate root patterns

Pitfalls & limitations

  • !Eventual consistency can complicate debugging and error handling
  • !Over-using events for internal logic creates unnecessary system complexity
  • !Failure to handle idempotency in event consumers leads to duplicate state processing

FAQ

What is the benefit of @TransactionalEventListener?
It ensures that an event is only processed after the current database transaction commits, preventing scenarios where an event triggers before the source data is actually saved.
Why use an outbox pattern?
It solves the dual-write problem by saving the event to a database table in the same transaction as the business data, ensuring messages are never lost if the message broker is temporarily unreachable.
How does this differ from standard Spring events?
Standard events are local and synchronous by default, whereas this skill extends that concept to external, distributed messaging using Kafka and cloud-stream configurations.

How it compares

Unlike manual message producer implementations, this pattern provides a structured, boilerplate-free way to maintain aggregate boundaries and database-to-event consistency.

Source & trust

282 stars📄 MIT🕒 Updated 2026-06-15
📄 Full skill instructions — original source: giuseppe-trisciuoglio/developer-kit
# 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:**
<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:

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

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/spring-boot-event-driven-patterns/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/giuseppe-trisciuoglio/developer-kit/spring-boot-event-driven-patterns/SKILL.md
  • Cursor: ~/.cursor/skills/giuseppe-trisciuoglio/developer-kit/spring-boot-event-driven-patterns/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/giuseppe-trisciuoglio/developer-kit/spring-boot-event-driven-patterns/SKILL.md

🚀 Install with CLI:
npx skills add giuseppe-trisciuoglio/developer-kit

Read the Master Guide: Mastering Agent Skills

Recommended Rules

View more rules

Recommended Workflows

View more workflows

Recommended MCP Servers

View more MCP servers

Take It Further

Maximize your productivity with these powerful resources

📋

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
📖

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid architecture & design patterns issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Architecture & Design Patterns and is published by Giuseppe Trisciuoglio, maintained in giuseppe-trisciuoglio/developer-kit.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.