Querying read models

Read models are retrieved through a repository interface that Spring DDD generates at startup. Declaring the interface is all that is required — the projection store starter provides the underlying implementation automatically.

JSON read models (JsonProjectionRepository)

For a @JsonProjection read model, declare a subinterface of JsonProjectionRepository and inject it where needed:

interface AccountSummaryRepository : JsonProjectionRepository<AccountSummary, AccountId>

Spring DDD registers the implementation at startup; no @Bean declaration or factory is needed.

Finding by identity

val summary: AccountSummary? = repository.findByIdOrNull(accountId)

findByIdOrNull returns the read model with the given id, or null if none exists.

For a GLOBAL-scope projection the read-model key is the projection’s name attribute, so pass that string as the id.

Paged retrieval

val page: Page<AccountSummary> = repository.findAll(PageRequest.of(0, 20))

findAll(pageable: Pageable): Page<T> returns a page of read models. Without a sort the results are ordered by read_model_id ASC. When a sort is present, every sort property must be a declared @QueryField column — sorting on the JSON blob is not supported.

Querying by @QueryField columns

On the JDBC store you can only query a @JsonProjection read model by fields that have been promoted to indexed query columns with @QueryField; derived finders and sort properties never reach inside the JSON blob.

For how to declare these columns — the @QueryField attributes, typed (NUMBER / TEMPORAL / BOOLEAN) columns, precision/scale, the dedicated-table rule, and the per-dialect DDL — see JSON projections. Derived finders that query against those columns are shown in Derived finders below.

In-memory read models (InMemoryProjectionRepository)

@InMemoryProjection read models expose the same findByIdOrNull and findAll entry points, plus additional query capabilities available only with in-memory storage.

Declare a subinterface to gain access to derived finders:

interface AccountSummaryRepository : InMemoryProjectionRepository<AccountSummary, AccountId>

Finding by identity

val summary: AccountSummary? = repository.findByIdOrNull(accountId)

Predicate queries (in-memory only)

Because read models are heap-resident, the in-memory repository supports arbitrary predicate-based queries. This capability is not available on JsonProjectionRepository or JPA-backed repositories.

// Find the single open account for a given owner — throws if more than one matches
val summary: AccountSummary? = repository.findOne(Predicate { it.owner == ownerId })

// Find all overdrawn accounts, sorted by balance ascending
val overdrawn: List<AccountSummary> = repository.findAll(
    Predicate { it.balance < BigDecimal.ZERO },
    Sort.by("balance")
)

// Paginate results matching a predicate
val page: Page<AccountSummary> = repository.findAll(
    Predicate { it.status == AccountStatus.ACTIVE },
    PageRequest.of(0, 20, Sort.by("balance").descending())
)

Full predicate API on InMemoryProjectionRepository<T, ID>:

fun findOne(predicate: Predicate<T>): T?
fun findAll(predicate: Predicate<T>): List<T>
fun findAll(predicate: Predicate<T>, sort: Sort): List<T>
fun findAll(predicate: Predicate<T>, comparator: Comparator<T>): List<T>
fun findAll(predicate: Predicate<T>, pageable: Pageable): Page<T>

findOne throws IllegalStateException if more than one read model matches; it returns null if none match. All predicate overloads perform a full in-memory scan.

Derived finders

Both JsonProjectionRepository and InMemoryProjectionRepository support Spring Data–style derived finder methods declared on a subinterface. The naming convention follows the same Spring Data derived-query grammar (e.g. findBy<Property>, findAllBy<Property>Between, findBy<A>And<B>).

Derived finders on JsonProjectionRepository

For the JDBC store, derived finders are restricted to properties annotated with @QueryField. Each query translates to a SQL predicate against the dedicated query column.

interface AccountSummaryRepository : JsonProjectionRepository<AccountSummary, AccountId> {

    // @QueryField(unique = true) property → single-result finder
    fun findByAccountNumber(number: String): AccountSummary?

    // collection result
    fun findAllByOwner(owner: OwnerId): List<AccountSummary>

    // paged result
    fun findAllByOwner(owner: OwnerId, pageable: Pageable): Page<AccountSummary>

    // range predicate on a @QueryField(type = NUMBER) property
    fun findAllByBalanceGreaterThan(threshold: BigDecimal): List<AccountSummary>
}

Spring DDD validates all declared derived finders at startup. A finder that references a property without @QueryField, or that uses a return type incompatible with the column’s unique setting, fails fast with a descriptive error.

Derived finders on InMemoryProjectionRepository

For in-memory read models, derived finders run as a reflective full scan over the heap-resident models. No @QueryField annotation is required — any readable property can appear in a finder name.

interface AccountSummaryRepository : InMemoryProjectionRepository<AccountSummary, AccountId> {

    fun findByOwner(owner: OwnerId): AccountSummary?
    fun findAllByStatus(status: AccountStatus): List<AccountSummary>
}

Bulk loading

Both repositories support retrieving multiple read models in a single call.

JSON store

val page: Page<AccountSummary> = repository.findAll(PageRequest.of(0, 100))

findAll(pageable: Pageable): Page<T> is available on JsonProjectionRepository. Results are ordered by read_model_id ASC when no sort is specified; sort properties must map to declared @QueryField columns.

In-memory store

InMemoryProjectionRepository additionally provides a sort-only overload that returns all matching read models without a page boundary:

val all: List<AccountSummary> = repository.findAll(Sort.by("balance").descending())

findAll(sort: Sort): List<T> returns every heap-resident read model, ordered reflectively on the given property. This overload is in-memory only and is well-suited for small-to-medium read-model populations where paging is not required.