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)

existsById

fun existsById(id: ID): Boolean

Returns true if at least one event exists in the event store for the given id. For aggregates that declare an @AggregateDeleted marker the check replays the stream (same cost as findById) so that a deleted aggregate correctly reports false.

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.

findAllById is intended for bulk reads such as reports or admin views. For the command path — where a command targets a single aggregate — use findById.

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 (JsonProjectionRepository, InMemoryProjectionRepository, derived query methods) is covered under CQRS.