Query Handlers
CQRS
Queries carry read intent, and query handlers process them. They receive a query, access the right projection through a ReadView, and return the result. Query handlers are the read-side mirror of command handlers, completing the CQRS pipeline.
For background on how query handlers complete the CQRS read pipeline, see Query Handlers concept.
Defining a Query Handler
Query handlers are defined with the @domain.query_handler decorator:
from protean import current_domain, read
from protean.core.projection import BaseProjection
from protean.core.query import BaseQuery
from protean.core.query_handler import BaseQueryHandler
from protean.fields import Float, Identifier, Integer, String
@domain.projection
class OrderSummary(BaseProjection):
order_id = Identifier(identifier=True)
customer_name = String(max_length=100)
status = String(max_length=20)
total_amount = Float()
@domain.query(part_of=OrderSummary)
class GetOrdersByCustomer(BaseQuery):
customer_id = Identifier(required=True)
status = String()
page = Integer(default=1)
page_size = Integer(default=20)
@domain.query(part_of=OrderSummary)
class GetOrderById(BaseQuery):
order_id = Identifier(required=True)
@domain.query_handler(part_of=OrderSummary)
class OrderSummaryQueryHandler(BaseQueryHandler):
@read(GetOrdersByCustomer)
def get_by_customer(self, query):
view = current_domain.view_for(OrderSummary)
results = view.query.filter(
customer_id=query.customer_id
)
if query.status:
results = results.filter(status=query.status)
return results.all()
@read(GetOrderById)
def get_by_id(self, query):
view = current_domain.view_for(OrderSummary)
return view.get(query.order_id)
The @read Decorator
The @read decorator marks methods as query handlers. It accepts a single
argument -- the query class to handle:
@read(GetOrdersByCustomer)
def get_by_customer(self, query):
...
Unlike @handle, the @read decorator:
- Does not wrap execution in a UnitOfWork
- Does not accept
start,correlate, orendparameters - Is intended exclusively for query handlers
Dispatching Queries
Dispatch queries with domain.dispatch():
# From an API endpoint or application layer
result = domain.dispatch(
GetOrdersByCustomer(customer_id="cust-123", status="shipped")
)
domain.dispatch() resolves the registered query handler, invokes the
correct method, and returns the result directly.
Comparison with domain.process()
| Aspect | domain.process(command) |
domain.dispatch(query) |
|---|---|---|
| Side | Write | Read |
| UoW | Yes | No |
| Returns | Optional | Always |
| Async | Supported | No (synchronous only) |
| Idempotency | Supported | Not needed |
Workflow
sequenceDiagram
autonumber
App->>Domain: dispatch(query)
Domain->>Query Handler: Route to handler method
Query Handler->>ReadView: view_for(Projection)
ReadView->>Database: Read query
Database-->>ReadView: Results
ReadView-->>Query Handler: Results
Query Handler-->>App: Return value
- Application dispatches query: The API layer creates a query object and
calls
domain.dispatch(). - Domain routes to handler: The domain resolves the registered query
handler and calls the matching
@read-decorated method. - Handler accesses ReadView: The handler method uses
domain.view_for()to get a read-only facade over the projection. - Results returned: The handler returns results, which pass through
domain.dispatch()back to the caller.
Three Levels of Read Access
Protean provides three levels of read abstraction:
| Level | API | When to Use |
|---|---|---|
| Pipeline | domain.dispatch(query) |
Named queries with validation, structured read logic |
| Direct | domain.view_for(Projection).query |
Simple filtering without handler ceremony |
| Raw | domain.connection_for(Projection) |
Complex queries needing database-specific features |
Query handlers operate at Level 1, providing the most structured approach.
Use domain.view_for().query (Level 2) for simple lookups that don't need a
handler. Use domain.connection_for() (Level 3) when you need the raw
database or cache client for technology-specific queries.
Error Handling
domain.dispatch() raises IncorrectUsageError when:
- The argument is not a query instance
- The query class is not registered in the domain
- No query handler is registered for the query
from protean.exceptions import IncorrectUsageError
try:
result = domain.dispatch(GetOrdersByCustomer(customer_id="123"))
except IncorrectUsageError as e:
# Handle missing handler or unregistered query
...
See also
Concept overview: Query Handlers — How query handlers process read intents in CQRS.