no-mixed-handler
A command changes aggregates, a query reads views: each handler keeps to the repositories of its side.
- Rule
tactical/no-mixed-handler- Category
- Tactical: how building blocks are written
- Reports
- A command handler that receives a query repository, a query handler that receives what writes
- Applies to
- The constructor parameters of every
CommandHandlerandQueryHandler - Turn off
"tactical/no-mixed-handler": "off"
Why
PlaceOrderHandler checks the order summary to decide whether the order may be placed. The view is shaped for reading: it may be denormalised, stale or partial, and it protects no rule. The other way round, a query that receives the outbox can change state while it reads.
The fix
A command decides from the aggregate, which keeps the rules; a query reads a view, and writes nothing. Keeping each side to its own repositories keeps both honest, without going as far as separate read and write models.
What it checks
The constructor parameters of each handler:
CommandHandlerNever receives a QueryRepository.QueryHandlerNever receives a CommandRepository, an Outbox, a UnitOfWork or an EventPublisher.A parameter counts by what its type extends: Orders extends CommandRepository<Order>, OrderSummaries extends QueryRepository<OrderSummary>.
What it reports
src/ordering/application/commands/place-order.command.ts:2
tactical/no-mixed-handler: The CommandHandler PlaceOrderHandler
receives OrderSummaries, a QueryRepository: keep commands and
queries apart.
src/ordering/application/queries/get-order-summary.query.ts:3
tactical/no-mixed-handler: The QueryHandler GetOrderSummaryHandler
receives UnitOfWork, a UnitOfWork: keep commands and queries apart.Fix it
Decide from the aggregate in a command
So that the decision is taken by what keeps the rules, a command handler loads the aggregate through its command repository and lets it decide.
export class PlaceOrderHandler extends CommandHandler<PlaceOrder> {
constructor(private readonly summaries: OrderSummaries) {
super();
}
}export class PlaceOrderHandler extends CommandHandler<PlaceOrder> {
constructor(private readonly orders: Orders) {
super();
}
}Read a view in a query, and nothing else
So that a read never changes state, a query handler receives only query repositories. A query that seems to need a write is a command, or a command followed by a query.
constructor(
private readonly summaries: OrderSummaries,
private readonly unitOfWork: UnitOfWork,
) {
super();
}constructor(private readonly summaries: OrderSummaries) {
super();
}Turn it off
rules: { "tactical/no-mixed-handler": "off" },On an existing project, prefer a baseline: new handlers keep to their side while you split the old ones.
See also
- Command handlers and Query handlers, the two sides
- Repositories and Views, what each side reads
tactical/no-misplaced-class, for where handlers and repositories live- Rules, every rule by category