Skip to content

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 CommandHandler and QueryHandler
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.

❌ Avoid: src/ordering/application/commands/place-order.command.ts
ts
export class PlaceOrderHandler extends CommandHandler<PlaceOrder> {
	constructor(private readonly summaries: OrderSummaries) {
		super();
	}
}
✅ Prefer: src/ordering/application/commands/place-order.command.ts
ts
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.

❌ Avoid: src/ordering/application/queries/get-order-summary.query.ts
ts
constructor(
	private readonly summaries: OrderSummaries,
	private readonly unitOfWork: UnitOfWork,
) {
	super();
}
✅ Prefer: src/ordering/application/queries/get-order-summary.query.ts
ts
constructor(private readonly summaries: OrderSummaries) {
	super();
}

Turn it off ​

alveolus.config.ts
ts
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 ​

Released under the MIT License.