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. |
The requested type is |
5. |
The requested type is |
6. Collection conversion |
The requested type is |
7. Wrapped generic |
The requested type is a generic wrapper such as |