Commands
A command expresses the caller’s intent to change system state.
Spring DDD dispatches commands through a type-safe command bus to a single handler that owns
the transaction; the bus itself is a pure dispatcher.
Commands are part of the CQRS feature provided by spring-ddd-starter-cqrs-jdbc (or a full
bundle such as spring-ddd-starter-jdbc) — see Choosing your starters.
Defining a command
Annotate any class with @Command to mark it as a command message.
The optional namespace and name attributes give the command a stable type identifier that is
independent of the JVM class path.
Mark exactly one field with @AggregateId to identify the target aggregate instance; the
framework uses this field to locate the aggregate on the non-creational handler path.
import de.dwittkoetter.ddd.annotation.AggregateId
import de.dwittkoetter.ddd.annotation.Command
@Command(namespace = "banking", name = "OpenAccount")
data class OpenAccount(
@AggregateId val accountId: AccountId,
val initialBalance: Money,
)
@Command(namespace = "banking", name = "DepositMoney")
data class DepositMoney(
@AggregateId val accountId: AccountId,
val amount: Money,
)
@Command carries the jMolecules Command stereotype as a meta-annotation, so
jMolecules-aware tooling recognises the stereotype automatically.
See Building blocks & stereotypes for the full annotation table.
|
Command handlers on a bean
Any Spring-managed @Component can host a @CommandHandler method.
The method must accept the command as its first parameter and return Unit (plain bean handlers; the aggregate-loading variant below returns the aggregate instead).
Inject collaborators through the constructor — method-parameter bean injection is not supported.
import de.dwittkoetter.ddd.annotation.ApplicationService
import de.dwittkoetter.ddd.cqrs.CommandHandler
@ApplicationService
class AccountCommandHandlers(
private val repository: BankAccountRepository,
) {
@CommandHandler
fun handle(command: OpenAccount) {
val account = BankAccount(command.accountId, command.initialBalance)
repository.save(account)
}
@CommandHandler
fun handle(command: DepositMoney) {
val account = repository.findById(command.accountId).orElseThrow()
account.deposit(command.amount)
repository.save(account)
}
}
By default each @CommandHandler method runs in its own REQUIRES_NEW transaction.
Override the propagation level with the propagation attribute:
import org.springframework.transaction.annotation.Propagation
@CommandHandler(propagation = Propagation.REQUIRED)
fun handle(command: DepositMoney) { ... }
For finer control — isolation level, timeout, rollback rules — annotate the method with a full
@Transactional in addition to @CommandHandler.
The transaction model
The command bus owns no transaction. Each handler unit opens its own transaction independently:
-
A bean handler (a
@Component@CommandHandlermethod) runs inREQUIRES_NEWby default, as governed by the@Transactionalmeta-annotation on@CommandHandler. -
An auto-managed aggregate handler (see below) runs in a framework-owned
REQUIRES_NEWthat wraps the load → handle → save sequence atomically. -
A query handler runs in a read-only transaction — see Queries.
Because the bus is a pure dispatcher, interceptor invocations happen outside any transaction boundary.
Dispatching commands
Inject CommandGateway and call send (synchronous) or sendAsync (fire-and-forget):
import de.dwittkoetter.ddd.annotation.ApplicationService
import de.dwittkoetter.ddd.cqrs.command.CommandGateway
@ApplicationService
class AccountApplicationService(private val gateway: CommandGateway) {
fun openAccount(accountId: AccountId, initialBalance: Money) {
gateway.send(OpenAccount(accountId, initialBalance))
}
fun depositAsync(accountId: AccountId, amount: Money) {
gateway.sendAsync(DepositMoney(accountId, amount))
}
}
send blocks the calling thread until the handler completes and propagates any handler exception
wrapped in a CommandExecutionException.
sendAsync submits the command to a background executor and returns immediately; exceptions inside
the handler are logged but do not propagate to the caller.
Both methods validate the command via Jakarta Bean Validation before dispatching.
Auto-managed aggregate handling
Instead of writing repository calls by hand, place @CommandHandler directly on the aggregate class (shown here as an event-sourced aggregate):
-
A secondary constructor annotated with
@CommandHandleris a creational handler — the framework calls the constructor and saves the resulting aggregate instance. -
A method annotated with
@CommandHandleris a non-creational handler — the framework loads the aggregate by the command’s@AggregateId, calls the method, and then saves the aggregate.
import de.dwittkoetter.ddd.cqrs.CommandHandler
class BankAccount : EventSourcingAggregateRoot<BankAccount, AccountId>() {
// Creational handler — framework instantiates and saves
@CommandHandler
constructor(command: OpenAccount) {
registerEvent(AccountOpened(command.accountId, command.initialBalance))
}
// Non-creational handler — framework loads by @AggregateId, calls, saves
@CommandHandler
fun handle(command: DepositMoney) {
require(command.amount.isPositive()) { "Deposit amount must be positive" }
registerEvent(MoneyDeposited(accountId = id, amount = command.amount))
}
}
The load-handle-save sequence runs inside a single framework-owned REQUIRES_NEW transaction.
No separate handler bean or repository call is required.
The propagation attribute of @CommandHandler has no effect on aggregate handlers — both auto-managed roots and component handlers that declare a managed aggregate parameter always run inside a framework-owned REQUIRES_NEW transaction.
|
This pattern works for both event-sourced and JPA aggregates. For the persistence details specific to each model, see Aggregates (event-sourced) and Repositories & command handling (JPA).
Component handlers with aggregate loading
A @Component @CommandHandler method may also declare a managed aggregate as a parameter.
The framework loads the aggregate before invoking the method and saves it after, with no
repository code required in the handler body.
import de.dwittkoetter.ddd.annotation.ApplicationService
import de.dwittkoetter.ddd.cqrs.CommandHandler
import de.dwittkoetter.ddd.cqrs.messaging.exception.AggregateNotFoundException
@ApplicationService
class AccountCommandHandlers {
// Nullable param — aggregate loaded if it exists; null passed if it does not
@CommandHandler
fun handle(command: OpenAccount, account: BankAccount?): BankAccount {
check(account == null) { "Account ${command.accountId} already exists" }
return BankAccount(command.accountId, command.initialBalance)
// return type is BankAccount → framework saves the return value
}
// Non-null param — AggregateNotFoundException if the aggregate does not exist
@CommandHandler
fun handle(command: DepositMoney, account: BankAccount) {
account.deposit(command.amount)
// return Unit → framework saves the (mutated) parameter
}
}
Nullable aggregate parameter — the framework attempts to load the aggregate by the command’s
@AggregateId; if the aggregate does not exist, null is passed to the handler.
Non-null aggregate parameter — the aggregate must exist; if it does not, an
AggregateNotFoundException is thrown before the handler is invoked.
Save-target rules:
-
Return the aggregate type → the framework saves the return value.
-
Return
Unit→ the framework saves the (mutated) parameter.
Inject collaborators through the constructor; method-parameter bean injection is not supported.
Handler parameters annotated with @MetadataValue are also supported — see
Message metadata and
Message interceptors.