Skip to content

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: