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 a full bundle such as spring-ddd-starter-jdbc) 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, ConcurrentModificationException is thrown.
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.
The maximum number of aggregates fetched per query is controlled by:
spring.ddd.eventsourcing.batch-size=100
The default is 100.
Larger id lists are split into chunks of this size.
|
|
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.
|
Reading CQRS read models ( |