Repositories & command handling
A JPA aggregate is persisted through a plain Spring Data JpaRepository — there is nothing
spring-ddd-specific about the repository interface itself.
The optional cqrs-jpa bridge adds auto-managed @CommandHandler support: it loads the aggregate
before a command method and saves it afterwards, matching the model used for event-sourced aggregates.
Repository
Declare a standard Spring Data JPA repository:
interface BankAccountRepository : JpaRepository<BankAccount, AccountId>
Calling save also publishes the aggregate’s buffered domain events — see Domain events & optimistic locking.
|
|
Auto-managed command handling
The spring-ddd-cqrs-jpa module (activated by spring-ddd-starter-cqrs-jpa, or the full bundle
spring-ddd-starter-jpa) provides a cqrs-jpa bridge that manages the aggregate lifecycle for you
when @CommandHandler is declared directly on an @Entity @Aggregate class.
How it works
-
Creational handlers — a constructor annotated with
@CommandHandlercreates a new aggregate instance. The bridge calls the constructor and then saves the new aggregate via the repository. -
Non-creational handlers — a method annotated with
@CommandHandlerupdates an existing aggregate. The bridge loads the aggregate by the id extracted from the command (via@AggregateId), invokes the method, and then saves the aggregate.
In both cases the load and save are performed transparently — you do not call the repository yourself.
Example
@Entity
@Table(name = "bank_accounts")
@Aggregate(namespace = "banking")
class BankAccount(
@EmbeddedId
@AttributeOverride(name = "value", column = Column(name = "id", nullable = false, updatable = false))
@AggregateId
override var id: AccountId = AccountId(),
) : AbstractAggregateRootEntity<BankAccount, AccountId>() {
var balance: BigDecimal = BigDecimal.ZERO
/** Creational handler — opens a new account. The bridge creates and saves the aggregate. */
@CommandHandler
constructor(cmd: OpenAccount) : this(cmd.accountId) {
balance = cmd.initialBalance.value
registerEvent(AccountOpened(id, cmd.initialBalance))
}
/** Non-creational handler — deposits money. The bridge loads, invokes, and saves. */
@CommandHandler
fun deposit(cmd: DepositMoney) {
balance = balance.add(cmd.amount.value)
registerEvent(MoneyDeposited(id, cmd.amount))
}
}
The command class for a non-creational handler must carry an @AggregateId field so the bridge
can locate the aggregate:
@Command
data class DepositMoney(
@AggregateId val accountId: AccountId,
val amount: Money,
)
|
|
Commands reach these handlers through the command bus — see CQRS for dispatch, gateways, and the full command rules.