@CommandHandler and @QueryHandler
@CommandHandler and @QueryHandler from @frenchexdev/ddd-cqrs-handlers reify the receivers for @Command and @Query messages. Each handler is registered with the Mediator under exactly one message type — single dispatch, no fan-out, no ambiguity.
What CQRS Handlers Reify
The single-dispatch discipline matters. Commands change state, and a command handled twice changes the state twice — duplicated charges, duplicated emails, duplicated provisioning. A command must have exactly one handler, and the handler is responsible for seeing the command once (often with idempotency keys to make the "once" survive retries).
Queries are looser — running a query twice returns the same answer twice, no side effect — but the convention is the same: one handler per query type, because the canonical answer should come from one place. The query handler decides whether to read from a fresh database query, a cached read model, or a search index; the call site does not care.
The @CommandHandler decorator's optional idempotencyKey: string argument names a field on the command. When set, the mediator (or a pipeline behaviour, see Part 46) deduplicates commands carrying the same key — useful for retried HTTP requests where the client wants exactly-once command execution semantics across network failures.
The Runtime: ddd-cqrs-handlers
decorators.ts declares the surface. @CommandHandler({ idempotencyKey? }) and @QueryHandler() stamp the appropriate metadata onto the handler class.
In our invented Subscription context:
import { CommandHandler, QueryHandler } from '@frenchexdev/ddd-cqrs-handlers';
import type { SubscriptionService } from '../services/subscription.service.js';
import type { SubscribeCustomerToPlanCommand, GetCustomerSubscriptionsQuery } from '../messages/index.js';
@CommandHandler({ idempotencyKey: 'idempotencyKey' })
export class SubscribeCustomerToPlanHandler {
constructor(private readonly service: SubscriptionService) {}
async handle(cmd: SubscribeCustomerToPlanCommand): Promise<Result<void, SubscriptionFailure>> {
return this.service.subscribeCustomerToPlan(cmd);
}
}
@QueryHandler()
export class GetCustomerSubscriptionsHandler {
constructor(private readonly readModels: CustomerSubscriptionsReadModelPort) {}
async handle(q: GetCustomerSubscriptionsQuery): Promise<readonly SubscriptionSummary[]> {
return this.readModels.findByCustomer(q.customerId);
}
}import { CommandHandler, QueryHandler } from '@frenchexdev/ddd-cqrs-handlers';
import type { SubscriptionService } from '../services/subscription.service.js';
import type { SubscribeCustomerToPlanCommand, GetCustomerSubscriptionsQuery } from '../messages/index.js';
@CommandHandler({ idempotencyKey: 'idempotencyKey' })
export class SubscribeCustomerToPlanHandler {
constructor(private readonly service: SubscriptionService) {}
async handle(cmd: SubscribeCustomerToPlanCommand): Promise<Result<void, SubscriptionFailure>> {
return this.service.subscribeCustomerToPlan(cmd);
}
}
@QueryHandler()
export class GetCustomerSubscriptionsHandler {
constructor(private readonly readModels: CustomerSubscriptionsReadModelPort) {}
async handle(q: GetCustomerSubscriptionsQuery): Promise<readonly SubscriptionSummary[]> {
return this.readModels.findByCustomer(q.customerId);
}
}The command handler is thin — it delegates to an application service. The query handler is thin — it reads from a read model. Neither contains domain logic; both translate from message to call.
Cross-Links
- Receive
@Commandand@Querymessages. - Register with the
@Mediator— single-dispatch. - Command handler invokes
@ApplicationServiceor@AggregateRootdirectly. - Query handler reads from
@Repositoryor@ReadModel. - Wrapped by
@PipelineBehaviorfor cross-cutting concerns (logging, validation, idempotency). - Each handler typically corresponds to a
@UseCase.
Back to the series index.