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_NEW transaction by default — never joining the already-committed publishing transaction. Override this via the propagation attribute (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 @EventListener machinery, so it is not bound by the single-parameter restriction stock listeners impose.

  • Interception and message metadata. It participates in @MessageHandlerInterceptor interception and supports @MetadataValue parameter 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.