Quick start
A minimal end-to-end slice using event sourcing and CQRS, provided by spring-ddd-starter-jdbc.
1. Define commands, events, and the aggregate
Mark events with @DomainEvent and commands with @Command; the field that
identifies the target aggregate carries @AggregateId.
import de.dwittkoetter.ddd.annotation.Command
import de.dwittkoetter.ddd.annotation.AggregateId
import de.dwittkoetter.ddd.annotation.DomainEvent
@Command
data class OpenAccount(@AggregateId val accountId: AccountId, val initialBalance: Money)
@Command
data class DepositMoney(@AggregateId val accountId: AccountId, val amount: Money)
@DomainEvent(namespace = "banking")
data class AccountOpened(val accountId: AccountId, val initialBalance: Money)
@DomainEvent(namespace = "banking")
data class MoneyDeposited(val accountId: AccountId, val amount: Money)
Mark the aggregate with @Aggregate and extend EventSourcingAggregateRoot
(it contributes @EventSourced, @AggregateId, @Version, and registerEvent).
The central event-sourcing feature is handling commands directly on the aggregate:
a @CommandHandler constructor opens a new account, and a @CommandHandler
method mutates an existing one. Each handler only calls registerEvent;
@EventSourcingHandler methods fold those events into state.
import de.dwittkoetter.ddd.annotation.Aggregate
import de.dwittkoetter.ddd.cqrs.CommandHandler
import de.dwittkoetter.ddd.eventsourcing.EventSourcingHandler
import de.dwittkoetter.ddd.eventsourcing.aggregate.EventSourcingAggregateRoot
@Aggregate(namespace = "banking")
class BankAccount private constructor() : EventSourcingAggregateRoot<BankAccount, AccountId>() {
override lateinit var id: AccountId
private set
var balance: Money = Money.ZERO
private set
@CommandHandler
constructor(command: OpenAccount) : this() {
id = command.accountId
registerEvent(AccountOpened(command.accountId, command.initialBalance))
}
@CommandHandler
fun deposit(command: DepositMoney) {
registerEvent(MoneyDeposited(command.accountId, command.amount))
}
@EventSourcingHandler
fun on(event: AccountOpened) {
id = event.accountId
balance = event.initialBalance
}
@EventSourcingHandler
fun on(event: MoneyDeposited) {
balance += event.amount // Money is a domain value object with an operator fun plus
}
}
Declare a repository interface and activate scanning with @EnableEventSourcingRepositories:
import de.dwittkoetter.ddd.eventsourcing.repository.EventSourcingRepository
import de.dwittkoetter.ddd.eventsourcing.repository.EnableEventSourcingRepositories
interface BankAccountRepository : EventSourcingRepository<BankAccount, AccountId>
@EnableEventSourcingRepositories
@SpringBootApplication
class BankingApplication
2. Handle a command
Spring DDD dispatches a command in one of two ways.
Auto-managed aggregate path
This is the idiomatic event-sourcing style, and the @CommandHandler members on
the aggregate above already implement it — there is no handler bean and no
repository call in your code:
-
A
@CommandHandlerconstructor is creational: the framework instantiates the aggregate from the command and saves the new account. -
A
@CommandHandlermethod is non-creational: the framework loads the aggregate by the command’s@AggregateId, invokes the method, and saves it.
Prefer this path whenever the command logic belongs on the aggregate and needs no external collaborators.
Component handler path
When a command must orchestrate several steps or call injected collaborators,
handle it in a @Component bean instead of annotating the aggregate. You then own
construction and persistence: replace the @CommandHandler constructor with a
plain factory, and let the bean save through the repository.
// On the aggregate, in place of the @CommandHandler constructor:
companion object {
fun open(command: OpenAccount): BankAccount {
val account = BankAccount()
account.id = command.accountId
account.registerEvent(AccountOpened(command.accountId, command.initialBalance))
return account
}
}
import de.dwittkoetter.ddd.cqrs.CommandHandler
import org.springframework.stereotype.Component
@Component
class OpenAccountHandler(
private val accounts: BankAccountRepository,
private val notifications: NotificationService,
) {
@CommandHandler
fun handle(command: OpenAccount) {
val account = BankAccount.open(command)
accounts.save(account)
notifications.welcome(command.accountId)
}
}
The CQRS section covers a richer component-handler variant, where the framework loads and saves an aggregate you declare as a handler parameter — so you skip the explicit repository call.
3. Query the read model
Define a query and a handler that returns a read model.
import de.dwittkoetter.ddd.annotation.Query
@Query
data class GetAccountBalance(val accountId: AccountId)
import de.dwittkoetter.ddd.cqrs.QueryHandler
import org.springframework.stereotype.Component
@Component
class AccountQueryHandler(private val summaries: AccountSummaryRepository) {
@QueryHandler
fun handle(query: GetAccountBalance): AccountSummary? =
summaries.findByAccountId(query.accountId)
}
4. Dispatch
Inject CommandGateway and QueryGateway wherever you need to drive the application.
import de.dwittkoetter.ddd.annotation.ApplicationService
import de.dwittkoetter.ddd.cqrs.command.CommandGateway
import de.dwittkoetter.ddd.cqrs.query.QueryGateway
import de.dwittkoetter.ddd.cqrs.query.query
@ApplicationService
class AccountService(
private val commands: CommandGateway,
private val queries: QueryGateway,
) {
fun openAccount(accountId: AccountId, initialBalance: Money) {
commands.send(OpenAccount(accountId, initialBalance))
}
fun getBalance(accountId: AccountId): AccountSummary? =
queries.query<AccountSummary?>(GetAccountBalance(accountId))
}
CommandGateway.send dispatches synchronously.
QueryGateway.query<R> infers the response type from the type parameter.
@ApplicationService marks the class as the application-layer entry point and is itself a Spring stereotype — see Application layering.
For event-sourcing depth (snapshotting, upcasting, optimistic locking), see Event-Sourcing. For projections and derived queries, see CQRS.