Repositories
An EventSourcingRepository is the single gateway for loading and saving an event-sourced aggregate:
it reconstructs aggregate state by replaying stored events (and snapshots) on findById, and appends new events on save.
The interface is provided by spring-ddd-eventsourcing; add spring-ddd-starter-eventsourcing-jdbc (or the full-stack bundle spring-ddd-starter-jpa) to make it runnable.
Aggregate structure is covered in Aggregates.
Declaring a repository
Extend EventSourcingRepository<T, ID> with one interface per aggregate type.
No implementation class is needed — the framework provides it automatically:
interface BankAccountRepository : EventSourcingRepository<BankAccount, AccountId>
Inject the interface into your services or command handlers as a regular Spring bean.
Activating scanning
With a starter on the classpath, repositories are discovered automatically from the @SpringBootApplication base package — no annotation is required.
@EnableEventSourcingRepositories is an optional override used to restrict or redirect the scanned packages.
Place it on a @Configuration class or on @SpringBootApplication itself:
@EnableEventSourcingRepositories(basePackages = ["com.example.banking"])
When present, @EnableEventSourcingRepositories takes over from auto-configuration for package resolution; all other auto-wiring (event store, snapshot store, serializers) remains unchanged.
Loading and saving
findById
fun findById(id: ID): Optional<T>
Loads the latest snapshot (if any), replays all events stored after the snapshot’s sequence number, and returns the reconstructed aggregate wrapped in an Optional.
Returns Optional.empty() if no events exist for the given id or if the aggregate has been logically deleted (marked with @AggregateDeleted).
val account: BankAccount = bankAccountRepository
.findById(AccountId("acc-1"))
.orElseThrow { NoSuchElementException("BankAccount acc-1 not found") }
save
fun save(aggregate: T): T
Appends all pending domain events to the event store, optionally creates a snapshot (based on the configured threshold), and then publishes the events via Spring’s ApplicationEventPublisher.
Returns the same aggregate instance with its pending-event collection cleared.
Optimistic concurrency is enforced: if another writer has already appended events at the same aggregate version, Spring’s OptimisticLockingFailureException is thrown, as a JPA aggregate’s version conflict is, so one retry policy covers both.
val account = bankAccountRepository
.findById(accountId)
.orElseThrow()
account.deposit(Money.of(100, "EUR"))
bankAccountRepository.save(account)
Bulk loading
fun findAllById(ids: Iterable<ID>): List<T>
Loads multiple aggregates of the same type in a bounded number of round trips — one snapshot query plus one events query per internal chunk — applying snapshots and upcasting exactly as findById does.
Duplicate ids are collapsed; the result is ordered by aggregate id ascending; ids with no events and logically-deleted aggregates are omitted, so the result list may be smaller than the distinct input.
How many aggregate ids one query carries is controlled by:
spring.ddd.eventsourcing.repositories.load.chunk-size=100
The default is 100.
Larger id lists are split into chunks of this size; the result is never limited by it.
|
|
Command handling
You can call this repository from a command handler yourself, or let the framework manage the load/save for you.
Place @CommandHandler directly on the event-sourced aggregate — a creational handler on a secondary constructor, a non-creational handler on a method — and the framework loads the aggregate through this repository before invoking the handler, then saves it afterwards, all in one framework-owned REQUIRES_NEW transaction.
This is the same auto-managed aggregate pattern the command pipeline uses for every aggregate type; the full rules and example (an event-sourced aggregate) are in Auto-managed aggregate handling. For the JPA-aggregate equivalent, see Repositories & command handling.