Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

@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);
  }
}

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.


Back to the series index.

⬇ Download