Communication PatternsMessaging

Command Pattern

Intent-driven imperative messages that instruct a service to perform an action — distinct from events, routed to a single handler, validated before dispatch, and central to the CQRS write side.

⏱ 11 min read

What it is

A command is an imperative message that says "do this": PlaceOrder, CancelReservation, TransferFunds. It expresses the intent of a sender directed at a specific service, and it expects that service to either carry out the action or reject it with a meaningful reason. Commands differ fundamentally from events: an event is a fact (something that has happened), while a command is a request (something the sender wants to happen, which may or may not succeed). Commands are addressed to a single handler — there is exactly one service responsible for processing a given command type — whereas events can be broadcast to many consumers.

The Command pattern is one of the core messaging primitives in Domain-Driven Design and CQRS. On the write side of a CQRS system, all mutations arrive as commands: the command is validated, the aggregate's business rules are evaluated, and if accepted, the aggregate emits one or more domain events that are persisted and published. Rejection is a first-class outcome — "I understood what you asked, but the current state of the system does not allow it" — returned as a structured error, not an exception.

Why it exists

Without a named command type, write operations arrive as generic HTTP PUT/POST calls with opaque JSON bodies. Routing logic, validation, and business rule evaluation are scattered across controllers and services. The Command pattern gives each write operation a named, typed object that encapsulates intent, carries all required data, and can be routed, validated, logged, queued, and replayed as a first-class unit. This separation also enables async command dispatch: the sender publishes the command to a queue and receives an acknowledgment that the command was received, while the actual processing happens asynchronously — decoupling API response latency from command execution time.

When to use

  • CQRS write side — all state-changing operations enter the system as typed commands dispatched to aggregate command handlers.
  • Async task initiation — the sender needs to initiate an operation but does not need the result synchronously; commands are queued and processed in order per aggregate.
  • Audit trail — storing command envelopes (who sent what, when, with what payload) provides a full audit log of all write operations independent of the resulting events.
  • Workflow orchestration — a saga or process manager sends commands to services as steps in a distributed workflow, receiving events back to advance state.

When not to use

  • Simple CRUD with no business rules — introducing command objects for trivial create/update/delete operations adds boilerplate without benefit; a straightforward service call is clearer.
  • Queries — commands mutate state; never use a command to read data. Use dedicated query models (CQRS read side) or simple service methods.
  • Broadcasting to multiple handlers — if multiple services need to react, use an event, not a command. Commands are addressed to one handler.

Typical architecture

A command is constructed by the sender (API controller, saga, UI), validated syntactically, then dispatched via a command bus (in-process) or a command queue (async). The command bus routes based on the command type to a registered handler. The handler performs authorisation, executes business logic against the aggregate, and either accepts (persisting resulting events) or rejects (returning a structured reason). One command type maps to exactly one handler.

API Layer
  │  POST /orders
  │  body: { items, customerId, shippingAddress }
  │
  ▼
Command Factory
  │  cmd = PlaceOrder {
  │    commandId: UUID,
  │    correlationId: UUID,
  │    issuedAt: ISO8601,
  │    issuedBy: userId,
  │    payload: { items, customerId, ... }
  │  }
  │
  ▼
Validation (syntactic) ──► reject with 400 if invalid
  │
  ▼
Command Bus / Queue ──► dispatches to single handler
  │
  ▼
PlaceOrderHandler
  ├─ Load Order aggregate
  ├─ Business rule checks (inventory, fraud, credit)
  ├─ Accept: emit OrderPlaced event → persist + publish
  └─ Reject: return CommandRejected { reason }

Pros and cons

Pros

  • Explicit intent — every write operation is a named, typed object with clear semantics.
  • Single handler per type — no ambiguity about which service owns the logic for a given operation.
  • Auditable — command envelopes can be logged, replayed, and analysed independently of side effects.
  • Async-compatible — commands can be queued for background processing without changing the sender interface.
  • Business rejection is a first-class outcome — structured rejection reasons, not exceptions.

Cons

  • Boilerplate — introducing command objects, handlers, and a bus adds ceremony for simple operations.
  • Async rejection feedback is harder — when commands are queued, the sender cannot receive a rejection synchronously; requires reply-to patterns or polling.
  • Learning curve — teams unfamiliar with CQRS/DDD need time to distinguish commands from events and learn the routing conventions.
  • Multiple handler frameworks exist (MediatR, Axon, NServiceBus) with differing conventions, making migration or standardisation across services difficult.

Implementation notes

Command envelope: Every command should carry: commandId (UUID, for idempotency), correlationId (UUID, linking the chain of commands and events), issuedAt (ISO 8601 timestamp), issuedBy (user/service identity), commandType (fully qualified name), and payload (the actual data). The envelope enables routing, auditing, tracing, and deduplication without coupling to payload content.

Validation before dispatch: Apply two layers of validation. Syntactic validation (types, required fields, format constraints) runs before dispatch and returns 400 to the sender — the command is not queued. Semantic/business validation (does the aggregate allow this in its current state?) runs inside the handler — if it fails, the command is rejected with a structured reason.

Command rejection vs failure: A rejection is a business-rule outcome ("insufficient inventory") — expected, logged at INFO, returned to the sender as a structured response. A failure is a technical error (database unavailable, timeout) — unexpected, logged at ERROR, triggers retry. Do not conflate the two.

One-handler-per-command rule: A command type must map to exactly one handler. If you find yourself routing one command to multiple handlers, you probably need an event (emit from the handler and let multiple consumers subscribe). Violating this rule leads to split logic, coordination problems, and partial execution under failure.

Common failure modes

  • Command treated as event: Multiple handlers registered for one command type; both execute, causing duplicate side effects.
  • No commandId for idempotency: On retry, the command is processed a second time, causing double billing, double inventory deduction, etc.
  • Business rejection thrown as exception: Handler throws an exception for a known business rule violation; the message broker retries it indefinitely, wasting resources.
  • Stale command processing: A command queued hours ago arrives when the aggregate's state has changed, and the handler applies it without checking current state first.

Decision checklist

  • Does the command envelope include a unique commandId for idempotency and a correlationId for tracing?
  • Is exactly one handler registered for this command type?
  • Is syntactic validation performed before dispatch (fail fast, no queuing of invalid commands)?
  • Does the handler distinguish business rejection (structured response) from technical failure (error/retry)?
  • If async: is there a mechanism to deliver rejection feedback to the original caller?
  • Is the command handler idempotent — i.e., processing the same commandId twice produces the same result as processing it once?

Example use cases

  • Order placement: PlaceOrder command dispatched to the order aggregate; handler validates credit limit and inventory, emits OrderPlaced event on success, or rejects with InsufficientInventory reason.
  • Funds transfer: TransferFunds { fromAccount, toAccount, amount } dispatched to the account aggregate; handler checks balance, atomically deducts and credits, emits FundsTransferred.
  • Saga orchestration: A flight-booking saga sends ReserveSeat, ChargeCard, and SendConfirmationEmail commands to three separate services as steps; compensating commands (ReleaseSeat, RefundCard) are sent on failure.
  • Event Notification — the fact that a command succeeded; emitted by the command handler.
  • CQRS — the architectural pattern that separates command (write) and query (read) models.
  • Saga Pattern — orchestrates multi-step workflows by issuing commands and reacting to events.
  • Message Routing — how commands are dispatched to the correct handler.

Further reading