Handling events
Domain events published by an aggregate are plain Spring application events.
Any Spring listener can observe them; spring-ddd-starter-domain-events additionally
provides the framework-managed @DomainEventHandler — a single annotation that bundles
Spring’s async, after-commit, new-transaction dispatch and adds multi-parameter handlers,
interception, and message-metadata support.
Live Spring listeners
Because published events are ordinary Spring application events, any
@EventListener method or Spring Modulith @ApplicationModuleListener can observe
them with no additional dependency.
Use a plain @EventListener when you want synchronous handling inside the publishing
transaction.
Spring Modulith’s @ApplicationModuleListener (async, after-commit, its own
transaction) overlaps with @DomainEventHandler; reach for @DomainEventHandler
when you also want Spring DDD’s interception and message-metadata support.
Framework-managed @DomainEventHandler
@DomainEventHandler (package de.dwittkoetter.ddd.domainevents) marks a method on a
Spring @Component as a framework-managed handler. In one annotation it gives you the
same delivery behaviour you would otherwise assemble from Spring’s @Async,
@TransactionalEventListener(phase = AFTER_COMMIT), and @Transactional — plus a couple
of capabilities those do not provide.
Standard Spring behaviour, bundled for convenience:
-
Async, after-commit dispatch. The handler runs after the publishing transaction has committed, so it never sees partial state from an in-progress unit of work.
-
Own transaction. It executes in its own
REQUIRES_NEWtransaction by default — never joining the already-committed publishing transaction. Override this via thepropagationattribute (e.g.Propagation.NOT_SUPPORTED) to run without a transaction. -
Failures logged, not rethrown. A handler failure is logged and not propagated back to the publisher (matching native async listener behaviour).
What @DomainEventHandler adds on top:
-
Multiple parameters. It registers its own dispatch adapter rather than borrowing Spring’s
@EventListenermachinery, so it is not bound by the single-parameter restriction stock listeners impose. -
Interception and message metadata. It participates in
@MessageHandlerInterceptorinterception and supports@MetadataValueparameter injection — see Message interceptors and Message metadata.
import de.dwittkoetter.ddd.domainevents.DomainEventHandler
@Component
class AccountNotifier {
@DomainEventHandler
fun on(event: MoneyDeposited) {
// runs async, after the deposit transaction commits,
// in its own REQUIRES_NEW transaction
}
}
To run the handler without a transaction — for example, when sending a notification
via an external service — set propagation = Propagation.NOT_SUPPORTED:
import de.dwittkoetter.ddd.domainevents.DomainEventHandler
import org.springframework.transaction.annotation.Propagation
@Component
class AccountNotifier(private val notificationService: NotificationService) {
@DomainEventHandler(propagation = Propagation.NOT_SUPPORTED)
fun on(event: MoneyDeposited) {
notificationService.send(event.accountId, event.amount)
}
}
Delivery guarantees
Plain Spring event dispatch is at-most-once. A crash after the publishing transaction commits but before the async handler runs loses the event — it is never replayed.
For at-least-once delivery backed by a durable store, see Spring Modulith Integration.