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 spring-ddd-starter-cqrs-jpa 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.

JpaRepository is a Spring Data abstraction — Spring DDD adds no wrapper, custom base class, or factory for it. Consult the Spring Data JPA reference for query methods, projections, and pagination.

Auto-managed command handling

The spring-ddd-cqrs-jpa module (activated by spring-ddd-starter-cqrs-jpa, or the full-stack bundle spring-ddd-starter-jpa) provides auto-managed @CommandHandler support 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 @CommandHandler creates a new aggregate instance. The framework calls the constructor and then saves the new aggregate via the repository.

  • Non-creational handlers — a method annotated with @CommandHandler updates an existing aggregate. The framework 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.

The repository behind it

The framework loads and saves the aggregate through a Spring Data JPA repository that it builds the way Spring Data builds your own:

  • Saving publishes the aggregate’s buffered domain events — see Domain events & optimistic locking.

  • JPA exceptions surface as Spring’s DataAccessException types.

  • Each command runs in a new transaction of the aggregate’s persistence unit.

  • `BeanPostProcessor`s that customize Spring Data repository factory beans apply to it too, Spring Boot’s repository metrics among them.

With several persistence units, the aggregate uses the EntityManagerFactory that manages its entity and the JpaTransactionManager bound to that factory. Without such a transaction manager, it uses the application’s only transaction manager, for example a JTA one. Handling the aggregate’s commands fails when no persistence unit or more than one manages the entity, or when no transaction manager fits.

The repository is not a bean: you cannot inject it, Spring Data REST does not export it, and @EnableJpaRepositories settings such as repositoryBaseClass, repositoryFactoryBeanClass and enableDefaultTransactions do not apply to it. When an aggregate needs them, declare your own repository and handle its commands on a bean — see Command handlers on a bean.

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 framework 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 framework 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 framework can locate the aggregate:

@Command
data class DepositMoney(
    @AggregateId val accountId: AccountId,
    val amount: Money,
)

@Aggregate is required for the auto-managed @CommandHandler support. It is not required for plain JPA persistence or event publication on its own.

Commands reach these handlers through the command bus — see CQRS for dispatch, gateways, and the full command rules.