Queries are the read side of CQRS. They represent a request for data and carry no side effects — they never modify state, raise events, or touch the write model. HexCore enforces this withDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/Indroic/HexCore/llms.txt
Use this file to discover all available pages before exploring further.
Query[TResult], a generic, frozen Pydantic model that declares its return type at the class level. Queries are always dispatched synchronously through InMemoryQueryBus.ask() and return the typed result directly to the caller.
The Query base class
Query is a frozen Pydantic model — instances are immutable and can be safely cached or passed across layers. The generic type parameter TResult communicates the expected return type to both the bus and the handler, enabling end-to-end type inference.
Defining a query
TResult type argument (OrderDTO, list[OrderDTO]) tells InMemoryQueryBus what type to expect from the handler, and your IDE will infer it automatically from await bus.ask(query).
AbstractQueryHandler
Each query type is served by exactly one handler that inherits from AbstractQueryHandler[TQuery, TResult].
The concrete
Query subclass this handler processes.The return type. Must match the
TResult declared in the Query class.Writing a query handler
Query handlers should use read-optimised data sources — dedicated read models, projections, or direct SQL views — instead of the write-side aggregate repositories.Registering and asking
from hexcore.application.cqrs.registry import HandlerRegistry
from app.queries import GetOrderByIdQuery, ListOrdersQuery
from app.query_handlers import GetOrderByIdHandler, ListOrdersHandler
from app.read_models import SqlOrderReadModel
read_model = SqlOrderReadModel(session)
registry = (
HandlerRegistry()
.register_query_handler(GetOrderByIdQuery, GetOrderByIdHandler(read_model))
.register_query_handler(ListOrdersQuery, ListOrdersHandler(read_model))
)
from hexcore.application.cqrs.in_memory_buses import InMemoryQueryBus
query_bus = InMemoryQueryBus(registry=registry)
A
MiddlewarePipeline is optional for queries. If you pass one, middlewares such as LoggingMiddleware will wrap each ask() call.from hexcore.application.cqrs.pipeline import MiddlewarePipeline
from hexcore.infrastructure.cqrs.middlewares import LoggingMiddleware
pipeline = MiddlewarePipeline().add(LoggingMiddleware())
query_bus = InMemoryQueryBus(registry=registry, pipeline=pipeline)
Complete example
FastAPI integration
Queries are always synchronous and local in HexCore. There is no async background dispatch, no queue, and no eventual consistency —
bus.ask() blocks until the handler returns a value. This is intentional: read operations must return data to the caller immediately. Never use a command bus for read operations, even if a read seems “expensive” — optimise the read model instead.Queries vs. Commands at a glance
| Aspect | Command | Query |
|---|---|---|
| Base class | Command | Query[TResult] |
| Return type | Any / None | Always typed TResult |
| Side effects | Yes (state changes) | None |
| Background routing | Supported (@background_command) | ❌ Never |
| Bus method | bus.dispatch(cmd) | bus.ask(query) |
| Middleware | Full pipeline | Optional pipeline |