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 @CommandHandler constructor is creational: the framework instantiates the aggregate from the command and saves the new account.

  • A @CommandHandler method 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.