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.