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-jpa (or the full-stack bundle spring-ddd-starter-jpa) — see 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.

@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.

@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.

A persistent projection’s read models can be sent to an external system; see Routing read models.

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.

@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.