Declaring & publishing
A domain event is an immutable record of something that has already happened in the domain.
Such events are the primary way other parts of the domain — and other bounded contexts — react to change without coupling to the originating code.
In Spring DDD, domain events are plain Kotlin classes annotated with @DomainEvent; the framework
publishes them after the originating transaction commits.
Declaring a domain event
Annotate any class with @DomainEvent to mark it as a domain event.
The namespace and name attributes give the event a stable type identifier that is independent
of the JVM class path — it is this identifier that is written to the event store and replayed to
projections, not the fully-qualified class name.
import de.dwittkoetter.ddd.annotation.AggregateId
import de.dwittkoetter.ddd.annotation.DomainEvent
import java.math.BigDecimal
import java.util.UUID
@DomainEvent(namespace = "banking", name = "AccountOpened")
data class AccountOpened(
@AggregateId val accountId: UUID,
val initialBalance: BigDecimal,
)
@DomainEvent(namespace = "banking", name = "MoneyDeposited")
data class MoneyDeposited(
@AggregateId val accountId: UUID,
val amount: BigDecimal,
)
@AggregateId on an event field identifies which aggregate instance the event belongs to.
Exactly one field per event class should carry this annotation.
@DomainEvent carries the jMolecules DomainEvent stereotype as a meta-annotation, so
jMolecules-aware tooling (ArchUnit, Spring Modulith architecture checks) recognises the stereotype
without a direct jMolecules compile dependency in your domain module.
See Building blocks & stereotypes for the full annotation table.
|
Publishing
Both event-sourced and JPA aggregates record pending events with registerEvent(…), inherited from
Spring Data’s AbstractAggregateRoot.
The repository publishes all buffered events after the aggregate is saved — that is, after the
originating transaction commits — then clears the buffer.
// inside BankAccount
fun deposit(amount: BigDecimal) {
require(amount > BigDecimal.ZERO) { "Deposit amount must be positive" }
balance += amount
registerEvent(MoneyDeposited(accountId = id, amount = amount))
}
For the mechanics specific to each aggregate model, see Aggregates (event-sourced) and Domain events & optimistic locking (JPA).
Publishing without an aggregate
Domain events are ordinary Spring application events: the repository emits each one through Spring’s
ApplicationEventPublisher. So for an event that is not tied to an aggregate’s state change, you can
publish it directly through the publisher — anything that uses Spring’s event machinery works.
import de.dwittkoetter.ddd.annotation.ApplicationService
import org.springframework.context.ApplicationEventPublisher
@ApplicationService
class AccountService(private val events: ApplicationEventPublisher) {
fun recordLogin(accountId: AccountId) {
events.publishEvent(AccountLoggedIn(accountId))
}
}
A directly published event reaches the same @EventListener and @DomainEventHandler handlers. The
difference is durability: it is published in-process only and is not written to the event store, so —
unlike an aggregate’s registerEvent — it is neither persisted nor replayable. Use registerEvent for
events that record an aggregate’s state change; use the publisher for transient, non-aggregate
notifications.
Handling the published events — live Spring listeners, the framework-managed @DomainEventHandler,
transaction semantics, and delivery guarantees — is covered in
Handling events.