From spring-skills
Guide Spring application events including publishing, listening, transactional event handling, async processing, and the event publication registry. Use when implementing event-driven communication within a Spring application, debugging silent event loss, or configuring @TransactionalEventListener.
How this skill is triggered — by the user, by Claude, or both
Slash command
/spring-skills:spring-eventsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Pattern:** Process
Pattern: Process
Conventions for event-driven communication within Spring applications. Targets Framework 7 / Boot 4.0. Most guidance applies to Framework 6.1+ / Boot 3.2+.
Mental model: Events are notifications of something that already happened. The publisher doesn't know or care who listens. Listeners are responsible for their own transactions, idempotency, and failure handling.
Do NOT Load for external messaging (Kafka, RabbitMQ) — use spring-kafka or the appropriate messaging skill. For module-boundary enforcement, combine with spring-modulith.
@EventListener, @TransactionalEventListener, and @ApplicationModuleListenerEvents are immutable records named in past tense — they describe something that happened, not a command:
public record OrderCompleted(String orderId, BigDecimal totalAmount, Instant completedAt) {}
String, BigDecimal, Instant, UUID, enums. Never entities or Spring beans| Scenario | Annotation | Why |
|---|---|---|
| In-memory cache/counter | @EventListener | Synchronous, same transaction. Failure rolls back publisher |
| Notification after success | @TransactionalEventListener | Only fires after commit |
| Cross-module side effect | @ApplicationModuleListener | Async, own TX, retry via registry |
| Derived write before commit | @TransactionalEventListener(BEFORE_COMMIT) | Still in publisher's transaction |
Before adding an event listener, ask:
@EventListener@TransactionalEventListener@ApplicationModuleListenerPublish within a @Transactional method, after the state change:
@Transactional
public Order completeOrder(String orderId) {
var order = orderRepository.findById(orderId).orElseThrow();
order.complete();
orderRepository.save(order);
events.publishEvent(new OrderCompleted(order.getId(), order.getTotalAmount(), Instant.now()));
return order;
}
Publishing outside a transaction with only @TransactionalEventListener handlers causes silent event loss — no error, no log. Use fallbackExecution = true on the listener if non-transactional publishing is intentional.
Before publishing an event, ask:
@Transactional? Without one, @TransactionalEventListener silently drops the eventCheck in this order:
@TransactionalEventListener requires a transaction. Without one, event is silently dropped. Set fallbackExecution = true or ensure @Transactional on publisher@Lazy bean? — listeners on @Lazy beans are never registered. No error at startup@EventListener for EntityCreatedEvent<Person> won't match unless the event implements ResolvableTypeProviderAFTER_COMMIT doesn't fire on rollback. Check transaction outcome@Order(n) controls synchronous invocation order (lower = first). Without @Order, execution order is undefined. @Order is meaningless for @Async listeners.
Synchronous listeners execute sequentially in the publisher's thread. 10 listeners at 50ms each = 500ms added to the publishing method. Move slow work to @ApplicationModuleListener.
For ordering, conditional listeners (SpEL), return value chaining, and generic event type erasure workarounds, load references/patterns.md.
@Async
@TransactionalEventListener
void on(OrderCompleted event) {
// Runs in separate thread after commit
// NO transaction context — JPA calls fail or use auto-commit
// NO context propagation (MDC, tracing) without TaskDecorator
}
Use TaskDecorator (e.g., ContextPropagatingTaskDecorator) on the executor for MDC/tracing propagation. See spring-framework references/scheduling-async.md.
Prefer @ApplicationModuleListener — it wraps @Async + @Transactional(REQUIRES_NEW) + @TransactionalEventListener + registry tracking in one annotation.
Customizable via a bean named applicationEventMulticaster. Setting a TaskExecutor on it makes ALL events async globally — including Spring's internal events (ContextRefreshedEvent), which causes startup ordering issues. Prefer per-listener @Async instead.
Built-in outbox pattern. Tracks event publications transactionally — failed listeners are retried.
spring:
modulith:
events:
republish-outstanding-events-on-restart: true
completion-mode: UPDATE # UPDATE, DELETE, or ARCHIVE
jdbc:
schema-initialization:
enabled: true
Automatically marks stuck publications as FAILED:
spring:
modulith:
events:
staleness:
published: PT1H
processing: PT1H
check-interval: PT10S
failedEvents.resubmit(
ResubmissionOptions.defaults()
.withBatchSize(100)
.withMinAge(Duration.ofMinutes(5))
.withFilter(pub -> pub.getCompletionAttempts() < 3)
);
For full registry configuration, completion modes, saga patterns, and event versioning, load references/patterns.md.
New event hierarchy for observing method failures:
MethodRollbackEvent — published by TransactionInterceptor on @Transactional rollback. Listen to centralize rollback logging/alertingMethodRetryEvent — published on @Retryable triggered retries@EventListener
void on(MethodRollbackEvent event) {
log.warn("Rollback in {}: {}", event.getMethod().getName(), event.getFailure().getMessage());
}
Since Framework 6.1: works with ReactiveTransactionManager. Use TransactionalEventPublisher to include TransactionContext as event source in WebFlux/R2DBC applications.
Test listeners as plain units — inject the event directly. For integration tests:
@Test
void shouldPublishOrderCompleted(AssertablePublishedEvents events) {
orderService.completeOrder("order-1");
assertThat(events).contains(OrderCompleted.class)
.matching(OrderCompleted::orderId, "order-1");
}
Testing gotchas:
@TransactionalEventListener in @Transactional tests — the test transaction never commits, so AFTER_COMMIT listeners never fire. Use @Commit or test via Spring Modulith's Scenario API@ApplicationModuleListener — async + own transaction. Use Scenario.publish(event).andWaitForStateChange(...) to await side effectsEventPublicationRepository and assert on publication stateFor async/cross-module testing with Spring Modulith's Scenario API, see the spring-modulith skill.
@Transactional with only @TransactionalEventListener handlers. Event is silently lost. Use fallbackExecution = true or ensure transactional context@EventListener on a @Lazy bean. Listener is never registered, events never arrive, no error@EventListener for EntityCreatedEvent<Person> never fires due to type erasure. Implement ResolvableTypeProvider on the event class@Async + @TransactionalEventListener without @Transactional(REQUIRES_NEW). Runs with no transaction — JPA calls fail or auto-commit. Use @ApplicationModuleListenerTaskExecutor on ApplicationEventMulticaster makes ALL events async, including Spring internals. Use per-listener @Async insteadStackOverflowError. Async: infinite growth. Design event flows as DAGsLoad on demand for specific topics:
references/patterns.md — Saga/choreography, compensation, event versioning, idempotent listeners, event enrichment, conditional listening, return value chaining, generic type erasure fix, scheduled resubmissionnpx claudepluginhub rynr/spring-skills --plugin spring-skillsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.