@Command and @Query Contracts
Command-Query Responsibility Segregation distinguishes imperative messages that change state from interrogative messages that return data without side effects. @Command and @Query from @frenchexdev/ddd-cqrs-messages reify the two as separate decorators so the asymmetry is visible at every call site.
What @Command and @Query Reify
The CQRS split, popularised by Greg Young building on Bertrand Meyer's command-query separation principle, treats writes and reads as fundamentally different operations. Commands are imperative — PlaceOrder, CancelSubscription, IssueRefund. They change state, they trigger domain events, they may fail and need to be retried. Queries are interrogative — GetUserById, ListOrdersForCustomer, FindActiveSubscriptions. They return data, they have no side effects (within the domain), they cannot fail in the same way commands fail.
The two decorators are intentionally separate even though one could be parameterised. Naming separately makes the asymmetry visible at every declaration. A @Command class signals this is going to mutate state; a @Query class signals this is asking a question. Reading a codebase, the decorator name is a signpost — the team that conflates them loses the orientation.
The Runtime: ddd-cqrs-messages
decorators.ts declares two nullary decorators. @Command() stamps __command + __commandMetadata with decoratorKind: 'Command'. @Query() stamps __query + __queryMetadata with decoratorKind: 'Query'. Both metadata interfaces are minimal — no options yet — because the discipline lives in the naming and the analyzer.
import { Command, Query } from '@frenchexdev/ddd-cqrs-messages';
import type { CustomerId, BillingPlanId } from '../domain/types.js';
@Command()
export class SubscribeCustomerToPlanCommand {
constructor(
public readonly customerId: CustomerId,
public readonly planId: BillingPlanId,
public readonly idempotencyKey: string,
) {}
}
@Query()
export class GetCustomerSubscriptionsQuery {
constructor(
public readonly customerId: CustomerId,
) {}
}import { Command, Query } from '@frenchexdev/ddd-cqrs-messages';
import type { CustomerId, BillingPlanId } from '../domain/types.js';
@Command()
export class SubscribeCustomerToPlanCommand {
constructor(
public readonly customerId: CustomerId,
public readonly planId: BillingPlanId,
public readonly idempotencyKey: string,
) {}
}
@Query()
export class GetCustomerSubscriptionsQuery {
constructor(
public readonly customerId: CustomerId,
) {}
}Both message classes carry only readonly constructor parameters. The convention is that messages are immutable — the handler receives the message as-is, no caller-side mutations after dispatch. The idempotencyKey on the command is a typical convention for distributed commands; the command handler may carry an idempotencyKey field in its decorator to extract it automatically.
The Codegen and Analyzer
Spec-first triplet. The analyzer enforces a Command or Query suffix at info severity (matching the decorator kind) plus single-per-file. A future cross-pattern rule will check that every command has exactly one command handler and every query has exactly one query handler — the single-dispatch discipline of the mediator made checkable.
The codegen emits a message registry — useful for the application's bootstrap to wire every (message, handler) pair against the mediator.
Cross-Links
- Dispatched to
@CommandHandler(for commands) or@QueryHandler(for queries). - Flow through
@Mediator— single-dispatch, one handler per message. - Command execution triggers
@DomainEventemission through the@AggregateRoot; query execution does not. - Query handlers return
@ReadModelshapes; command handlers returnResulttyped values.
Back to the series index.