Skip to content

Command

Turning a request into an object so it can be queued, retried, logged, undone or authorised — and the idempotency that queueing forces on you.

Intermediate18 minUpdated 8 August 2026

Command turns "do this thing" into an object. Once an action is a value rather than a call, you can do things to it that you cannot do to a method invocation: put it in a queue, write it to a log, ask whether the caller may run it, run it later, run it twice, or reverse it.

That list is the whole justification. If none of those apply, Command is a class where a function call would do.

From call to object

// A call: happens now, cannot be inspected, queued or reversed
inventory.transfer(itemId, fromWarehouse, toWarehouse, quantity);
// An object: exists before it happens, and after
class TransferStock {
  constructor(itemId, from, to, quantity) { /* … */ }
  execute(inventory) { inventory.move(this.itemId, this.from, this.to, this.quantity); }
  undo(inventory)    { inventory.move(this.itemId, this.to, this.from, this.quantity); }
}
Conceptual diagramnoyesCaller createsTransferStockQueue / log / auditAuthorised?Reject with reasonexecute()Push onto historyundo() available
Diagram, as text
  • Caller creates TransferStock leads to Queue / log / audit
  • Queue / log / audit leads to Authorised?
  • Authorised? leads to Reject with reason (no)
  • Authorised? leads to execute() (yes)
  • execute() leads to Push onto history
  • Push onto history leads to undo() available

The command carries everything needed to run: no hidden context, no reliance on what the user is currently looking at. That property is what makes the rest possible.

What being an object enables

Queueing and deferral. A command can be created now and executed later, by another thread or another process. This is what every background job system is.

Retry. A failed command still exists and can be run again — provided it is idempotent, which is the subject of the next section.

Undo. A command that knows how to reverse itself, kept on a stack, gives you undo for free. This is the classic motivation and the least common in server code.

Authorisation and audit. may(user, command) is a single dispatch point rather than a check scattered through every handler, and the same object serialised is an audit record of exactly what was requested.

Testing. Constructing a command and asserting on its fields is simpler than driving a UI to produce a call.

Macro commands. A command containing commands executes them as one, and reverses them in reverse order. That is Composite applied to Command.

Queueing forces idempotency on you

The moment a command can be retried, it must be safe to run twice, and this is where implementations go wrong.

A queue that guarantees at-least-once delivery — which is nearly all of them — will occasionally deliver the same command twice: a worker crashed after doing the work and before acknowledging, a network partition caused a redelivery, an operator replayed a dead-letter queue. If ChargeCard is not idempotent, that is a duplicate charge.

The fix is a key carried by the command itself:

class ChargeCard {
  constructor(orderId, amount, idempotencyKey) { /* … */ }
}

The handler records the key with the result before acknowledging, and a second execution with the same key returns the first result rather than repeating the work. The key must be generated by whoever created the command, not by the handler — a handler-generated key is different on each delivery and guards nothing.

Command, CQRS and the naming collision

The word "command" is overloaded and the collision causes real confusion.

  • The Command pattern here is a request as an object, usually with an execute.
  • A command in CQRS is a message expressing intent to change state, handled by exactly one handler, contrasted with a query.
  • A command in an event-sourced system is what is validated and, if accepted, turned into one or more events.

They are related — all three make an intention explicit and inspectable — but only the first necessarily has an execute method on it. In most modern codebases the second is what people mean, and the command object is a plain data record with a separate handler. That separation is usually better: the data can be serialised, versioned and stored without dragging behaviour and its dependencies along.

When not to use it

A single, immediate, non-reversible call. Wrapping it in an object adds a file and buys none of the six benefits.

Undo you will not implement. Writing undo() because the pattern has one, when no user-facing undo exists, produces untested reverse logic that will be wrong when someone finally uses it.

As a universal calling convention. Turning every method into a command produces a codebase where nothing can be found — execute is not a name, and the call graph disappears.

Check your understanding

Common mistakes

Queueing commands without idempotency keys. At-least-once delivery makes duplicate execution a certainty.

Generating the idempotency key in the handler. It differs per delivery and deduplicates nothing.

Writing undo() because the pattern has one. Untested reverse logic that will be wrong when it is first used.

Treating undo as a time machine. It is compensation, and it is unsound if anything depended on the original effect.

Commands that capture ambient context. A command relying on "the currently selected item" cannot be queued, replayed or audited.

Making everything a command. execute is not a name, and a uniform interface erases the call graph.

Behaviour and dependencies on the message. Serialising a command that holds a database connection does not work; separate the record from its handler.

Practice

Design a background job system's command contract for a team: what a command may contain, what guarantees the runner provides, and what a handler must implement. Justify the constraints.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Start from what the runner can promise, since that determines what handlers must cope with.

Show a strong answer

Start with the runner's guarantee, because everything else follows from it: at-least-once delivery, no ordering across commands, and a bounded number of attempts before a dead-letter queue. Promising exactly-once would be a lie — the acknowledge step can always fail after the work succeeds — and teams that believe it write handlers that corrupt data when the lie is exposed. Stating "at least once" up front makes idempotency an obvious requirement rather than an advanced topic.

What a command may contain:

  • Plain serialisable data only. No service references, no connections, no functions. The command must survive a round trip through storage.
  • An idempotency key set by the creator, part of the payload, stable across redeliveries.
  • A schema version. Commands outlive deploys: a command enqueued by the old version will be handled by the new one, so the payload shape is a compatibility surface exactly like an API.
  • Identifiers, not snapshots of mutable state — with the caveat that the handler must then tolerate the referenced entity having changed or been deleted.

The constraint that will get pushback: no ambient context. People want to capture "the current user" implicitly. It must be an explicit field, or the command cannot be replayed from a dead-letter queue months later.

What a handler must implement:

  • Idempotent execution, verified by a test that runs the handler twice and asserts one effect. I would make this test mandatory, because idempotency claimed in review and not tested is idempotency that does not exist.
  • A classification of its own failures into retryable (a timeout, a 503) and terminal (invalid payload, entity deleted). Retrying a terminal failure burns the attempt budget and delays the dead-letter, which is where a human would have seen it.
  • Tolerance of a changed world. By the time it runs, the order may be cancelled. Handling that is the handler's job, and "it was valid when enqueued" is not a defence.

What the runner provides beyond delivery:

  • Exponential backoff with jitter, so a downstream outage does not produce a synchronised retry storm.
  • A dead-letter queue with the original payload and the failure, plus an alert. A dead-letter nobody looks at is a silent data-loss mechanism.
  • A replay tool that re-enqueues from the dead-letter — which is only safe because idempotency was mandatory.
  • Correlation identifiers carried from the originating request, or debugging an async failure means reading unlinked logs.

The trade I would name explicitly: these rules make writing a handler more work than writing a function. That is the actual cost of making an operation deferrable, and the alternative is not a cheaper system but an unreliable one.

A collaborative document editor needs undo. Compare a command-history approach with storing document snapshots, and say how multi-user editing changes the answer.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Ask what 'undo' means when someone else has edited since your change.

Show a strong answer

Single user, command history is clearly better. Each edit is a command with an inverse; undo pops the stack and applies it. Memory is proportional to the number of edits rather than to document size times edit count, redo is the same stack in the other direction, and the history is a useful artefact — it can be shown to the user or persisted.

Snapshots are simpler and pay for it. Copying the document per keystroke is unworkable for anything large; even with structural sharing you store far more than the commands would, and you gain nothing except not having to write inverses.

Multi-user editing changes the answer, and the change is not a detail — it is the whole problem.

The moment two people edit concurrently, "undo my last change" stops being "revert to the previous state", because the previous state included none of the other person's work. Reverting the document wholesale would delete their edits, which is unambiguously wrong.

  • Snapshots become useless. A snapshot is a whole-document state, and there is no whole-document state to return to that does not discard someone else's work. This rules them out entirely rather than merely making them expensive.
  • Command history is necessary but not sufficient. Undo must apply my inverse to the current document, not restore a past one — so the inverse has to be transformed against every change that happened since. Deleting characters 5–10 is wrong if someone inserted a paragraph at position 3.

What the design actually needs, and this is the honest answer:

  • Operations expressed against positions that survive concurrent edits — either transformed against intervening operations, or expressed against stable identifiers rather than indices so they remain meaningful regardless of what else changed. These are the two established approaches, and both exist precisely because naive inverses do not compose under concurrency.
  • Per-user undo stacks, since undo is a user-scoped concept. A global stack lets me undo your work, which every user will report as a bug.
  • A definition of what happens when an undo is no longer meaningful — my inverse targets text you have since deleted. Silently doing nothing and silently doing something wrong are both bad; this needs a product decision.

The takeaway: Command with undo() is the right structure, and single-user undo makes it look far simpler than it is. Concurrency does not break the pattern; it reveals that the inverse must be defined relative to a moving document rather than a fixed history — which is a different and much harder problem than the pattern's textbook presentation suggests.

Key takeaways

  • Command makes a request an object, enabling queueing, retry, undo, authorisation, audit, testing and composition into macros.
  • If none of those apply, it is a class where a function call would do.
  • Queueing forces idempotency. At-least-once delivery is the norm, and the key must come from the command's creator.
  • Undo is compensation, not a time machine — unsound if anything depended on the original effect. Name it accordingly in server code.
  • Separate the command record from its handler, so the message stays serialisable and the dependencies stay injectable.
  • Commands must carry everything they need; ambient context makes them unqueueable and unreplayable.
  • The word collides with CQRS commands and event-sourcing commands — related ideas, different mechanics.

Further reading

  • Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
  • Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns (Addison-Wesley, 2003) — command messages, guaranteed delivery and dead letters.
  • Greg Young, "CQRS Documents" (2010) — commands as intent, separated from queries.
  • Vaughn Vernon, Implementing Domain-Driven Design (Addison-Wesley, 2013) — application commands and handlers.
  • Clarence Ellis and Simon Gibbs, "Concurrency Control in Groupware Systems" (ACM SIGMOD, 1989) — operational transformation and concurrent undo.

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonState
Lessons in this track