Query Handlers
Query handlers are the read-side counterpart of command handlers. They process queries -- named, validated read intents -- and return results from projections.
Where command handlers mutate state through aggregates, query handlers read
state from projections. This separation is fundamental to CQRS: writes flow
through domain.process(command), reads flow through
domain.dispatch(query).
Facts
Query handlers are connected to a projection.
Query handlers are always associated with a single projection via part_of.
This mirrors how command handlers are connected to aggregates, but on the
read side.
A query handler contains multiple handler methods.
Each method in a query handler is decorated with @read and handles a
specific query type. A handler can have methods for different queries,
all targeting the same projection.
Handler methods use @read, not @handle.
The @read decorator is intentionally distinct from @handle. While
@handle wraps execution in a UnitOfWork (for write-side consistency),
@read executes the method directly with no transaction wrapping.
Query handlers always return values.
Unlike event handlers (which return nothing) and command handlers (which
optionally return values), query handlers always return data. The return
value from the handler method passes through domain.dispatch() to the
caller.
No UnitOfWork wrapping.
Query handlers do not create transactions. Reads are stateless and should
never cause side effects. The absence of UoW is enforced by the @read
decorator.
Query handlers are synchronous only.
Unlike command and event handlers, query handlers have no async mode, no stream subscriptions, and no event store involvement. They execute synchronously and return immediately.
One handler per query.
Each query can only be handled by one handler method. This mirrors the command handler constraint and ensures unambiguous routing.
Best Practices
Keep handlers thin.
Query handlers should delegate to ReadView for data access. They should transform and filter, not compute or aggregate. Complex read logic belongs in the projection design, not the handler.
Use ReadView, not repositories.
Access projection data through domain.view_for(Projection) which
returns a read-only facade. This enforces CQRS separation and prevents
accidental mutations.
Validate through query fields.
Leverage query field constraints (required, min_value, max_value,
choices) for input validation. The query object validates its fields
before reaching the handler.
Next steps
For practical details on defining and using query handlers in Protean, see the guide:
- Query Handlers -- Defining
handlers, using
@read, dispatching queries.
For related concepts:
- Projections -- The read models that query handlers operate on.
- Command Handlers -- The write-side counterpart.