Queries

A query asks a question without changing state. Spring DDD dispatches queries through a type-safe query bus to a single matching handler that returns the result; the bus itself owns no transaction. Queries are part of the CQRS feature provided by spring-ddd-starter-cqrs-jdbc (or a full bundle such as spring-ddd-starter-jdbc) — see Choosing your starters.

Defining a query and a handler

Annotate any class with @Query to mark it as a query message. The optional namespace and name attributes give the query a stable type identifier independent of the JVM class path.

import de.dwittkoetter.ddd.annotation.Query

@Query(namespace = "banking", name = "GetAccountBalance")
data class GetAccountBalance(val accountId: AccountId)

Handle the query with a @QueryHandler method on any Spring-managed @Component. The method must accept exactly one parameter (the query) and return a non-Unit result. @QueryHandler is meta-annotated with @Transactional(readOnly = true), so the method executes inside a read-only transaction by default.

import de.dwittkoetter.ddd.annotation.ApplicationService
import de.dwittkoetter.ddd.cqrs.QueryHandler

@ApplicationService
class AccountQueryHandlers(
    private val summaryRepository: AccountSummaryRepository,
) {

    @QueryHandler
    fun handle(query: GetAccountBalance): AccountSummary? =
        summaryRepository.findByAccountId(query.accountId)
}
Annotate the read model a query returns — here AccountSummary — with @ReadModel from de.dwittkoetter.ddd.annotation. On a class it is a passive stereotype (jMolecules @QueryModel) that architecture tooling can discover, documenting the type’s role as a query model; it attaches no runtime behaviour. It is the same annotation that marks a projection listener’s read-model parameter (see Projections).
The read models and projections that populate the repository above are covered in Projections.

Dispatching a query

Inject QueryGateway and call the reified query<R>(…​) extension. The type parameter R lets the framework capture the expected response type without a manual KType reference.

import de.dwittkoetter.ddd.annotation.ApplicationService
import de.dwittkoetter.ddd.cqrs.query.QueryGateway
import de.dwittkoetter.ddd.cqrs.query.query

@ApplicationService
class AccountApplicationService(private val gateway: QueryGateway) {

    fun getBalance(accountId: AccountId): AccountSummary? =
        gateway.query<AccountSummary?>(GetAccountBalance(accountId))
}

An optional timeout parameter caps the handler’s execution time. The bus raises NoQueryHandlerException when no handler matches and AmbiguousQueryHandlerException when a rule ties. When you dispatch through QueryGateway, those — together with a handler’s own exception and timeouts — surface wrapped in QueryExecutionException, with the original available on QueryExecutionException.cause.

Return-type matching

For each (queryClass, responseType) pair the bus evaluates the following rules in order and selects the first match. Ties within a single rule throw AmbiguousQueryHandlerException.

Rule Match condition

1. Exact

The handler’s return type equals the requested type exactly.

2. Supertype

The handler’s return type is assignable to the requested type (same nullability).

3. Non-nullable satisfies nullable

The requested type is nullable; the handler’s return type is non-nullable (exact or supertype match ignoring nullability).

4. Optional<T> unwrap

The requested type is T? and the handler returns Optional<T>. The bus calls Optional.orElse(null) and returns the unwrapped value.

5. Optional<T> forward

The requested type is Optional<T> and the handler returns T? or Optional<T>. A nullable handler result is wrapped in Optional.ofNullable; an Optional handler result is forwarded as-is.

6. Collection conversion

The requested type is List<T> or Set<T> and the handler returns a Collection<T>. The bus calls toList() or toSet() accordingly.

7. Wrapped generic

The requested type is a generic wrapper such as Page<AccountSummary>, and a handler returning that same wrapped type is selected.