Queries
A query is an immutable, validated read intent, a named request for data on the read side of CQRS. Where a command expresses intent to change state, a query expresses intent to read it.
Queries are the input to query handlers. A query carries the parameters a read
needs (an identifier, a filter, a page number); the handler answers it from a
projection and returns the result. Queries flow through domain.dispatch(query),
mirroring how commands flow through domain.process(command).
Facts
Queries target a projection.
A query is associated with a projection (a read model) via part_of, not with an
aggregate. This places it firmly on the read side: queries describe reads against
the denormalized views that projectors maintain.
Queries are immutable.
Once constructed, a query's fields cannot be changed, assigning to a field
raises IncorrectUsageError. To issue a different read, build a new query.
Queries are validated at construction.
Field constraints (required, min_value, max_value, choices, and so on) are
enforced when the query is created, before it reaches a handler. An invalid query
raises ValidationError immediately.
Queries carry only basic data.
A query holds simple field types (and value objects). The parameters of a read.
It cannot contain associations (HasOne, HasMany, Reference); those belong
to aggregates.
Queries are named for what they return.
Commands are imperative (PlaceOrder); queries are named for their result
(GetOrderById, SearchOrders, ListActiveCustomers).
Queries are lightweight.
Unlike commands and events, queries carry no metadata, stream, or event-store concerns. They are plain read-intent DTOs, dispatched synchronously and answered immediately.
A query can declare its result type.
A query may state the type its handler returns by subscripting the base:
class GetOrderById(BaseQuery[OrderSummary]). domain.dispatch(query) then
resolves to that type at the call site instead of Any, checked by both mypy
and pyright. Declaring the result type is optional and changes nothing at
runtime.
Best Practices
Keep queries to read parameters.
A query is a parameter object, not a place for behavior. It should not reach into repositories or compute results. That is the query handler's job.
Model one query per read shape.
Prefer distinct queries (GetOrderById, SearchOrders) over a single
parameter-heavy query with many optional fields. Named reads are clearer and
validate more precisely.
Next steps
For practical details on defining queries and answering them, see the guide:
- Query Handlers: Defining
queries, the
@readdecorator, and dispatching withdomain.dispatch().
For related concepts:
- Query Handlers: The read-side handlers that answer queries.
- Projections: The read models queries target.
- Commands: The write-side counterpart (intent to change state).