Design patternBehavioral Patterns
Command
Wrap an action and its arguments in an object so you can queue, log, retry or undo it separately from the code that triggers it.
- Complexity
- Low
- Seen in the wild
- High
- Also known as
- Action, Transaction
Intent
Turn a request into an object that carries everything needed to perform it later: the receiver, the arguments, and any state needed to reverse it. The code that triggers the action (a button, a scheduler, an API handler) then only holds a command and calls execute, without knowing what the action does.
Forces
- Trigger now vs run later: the user clicks now, but the work goes on a queue, waits for a retry, or runs in a batch.
- Doing an action vs remembering it: undo, audit trails and replay need a record of what happened, not just its effect.
- Many triggers vs one behavior: a menu item, a keyboard shortcut and an API call should all run the same operation.
- Thin callers vs rich operations: the invoker should not know the receiver's API, but the operation needs it.
Therefore
Package each operation as an object with an execute method (and undo when needed), and have callers store and run those objects instead of calling the receiver directly.
The command holds a reference to the receiver and the arguments. The invoker keeps commands in a history, a queue or a log. For undo, a command records what it needs to reverse itself at execute time. In languages with closures, a command without undo is a function value; a pair of closures { run, undo } covers many undo cases too. Use classes when commands need names, serialization, or inspection.
Structure
Diagram source (Mermaid)
classDiagram
class StockLedger {
-StockCommand[] history
+run(cmd) void
+undoLast() void
}
class StockCommand {
<<interface>>
+execute() void
+undo() void
}
class Receive {
-String sku
-int qty
+execute() void
+undo() void
}
class Pick {
-String sku
-int qty
+execute() void
+undo() void
}
class CycleCount {
-int before
+execute() void
+undo() void
}
class Inventory {
+qty(sku) int
+set(sku, n) void
}
StockLedger o-- StockCommand
StockCommand <|.. Receive
StockCommand <|.. Pick
StockCommand <|.. CycleCount
Receive --> Inventory
Pick --> Inventory
CycleCount --> InventoryParticipants
- Command (
StockCommand): the interface the invoker calls. Usuallyexecute, plusundoand a label if the invoker needs them. - Concrete commands (
Receive,Pick,CycleCount): bind a receiver to arguments, and capture the state needed for undo when they run. - Receiver (
Inventory): the object that does the real work. It knows nothing about commands. - Invoker (
StockLedger): runs commands and keeps the history, queue or log. It never looks inside a command. - Client: creates commands with their receiver and arguments and hands them to the invoker.
Use it when
- You need multi-level undo or redo for user actions.
- Actions must be queued, scheduled, retried, or run on another worker, and you want them as data.
- You need an audit log of what was done, in order, that can be replayed to rebuild state.
- The same operation is triggered from several places (toolbar, shortcut, command palette, API) and should behave identically.
- You want to group several operations into one macro or transaction that succeeds or rolls back together.
Do not use it when
- You only need to pass behavior to be called later, with no undo, queue, history or logging. Pass a closure or function reference (
onClick={() => save(doc)}); aSaveCommandclass adds nothing. - The "queue" is a background job system. Put plain serializable messages (
{ type: "ship", orderId }) on the queue and map types to handlers. Command objects holding live receivers can't be serialized across processes. - Undo is the only goal and the state is small or immutable. Keep previous versions of the state, or use Memento snapshots, instead of writing an inverse for every operation.
- The operation is a single call from a single place. Call the receiver directly.
- The variation is how a result is computed, not which action runs. That is Strategy.
Application grill
Inspect first: the actions that need queueing, retry or undo, and whether a job system already runs (BullMQ, pg-boss, Celery, Sidekiq, SQS, platform queues); how command arguments would be serialized, since a queued command outlives the code version that queued it.
Missing pieces: asynchronous execution needs a queue. Prefer what the platform already provides (Vercel Queues, Cloudflare Queues, SQS); on an existing Postgres, pg-boss or SKIP LOCKED polling adds no new service; Redis plus BullMQ is the step up when throughput needs it. An undo stack for a session is in-memory; persisted undo is a table with a retention policy.
Ask before you build:
- Scale: how many commands per second, and how large are their payloads (queues bill per message and cap payload size)?
- Performance: how soon must a command run - immediately, or is seconds-later acceptable?
- Reliability: on failure, what happens - retry, dead-letter - and is the handler safe to run twice (idempotent-consumer)?
- Budget: managed queue versus Postgres-backed polling - whose operational budget decides?
- Implementation: who owns argument schema stability across deploys, and how are in-flight commands migrated?
Implementation steps
- Define the command interface from what the invoker needs:
execute, andundoorlabelonly if they are used. - For each operation, create a command that takes the receiver and arguments in its constructor.
- In
execute, call the receiver. If the command supports undo, capture the prior state here, not in the constructor. - Make the invoker record a command only after
executesucceeds, so a failed action never lands in the history. - Point every trigger (button, shortcut, API route) at the invoker instead of the receiver.
- Test each command's
executefollowed byundoreturns the receiver to its original state.
Example
A warehouse tool records stock movements. Clerks receive stock, pick it for orders, and correct counts after a cycle count. Every action is logged, a failed pick is refused without touching stock, and the last action can be undone.
class Inventory {
private stock = new Map<string, number>();
qty(sku: string): number {
return this.stock.get(sku) ?? 0;
}
set(sku: string, n: number): void {
this.stock.set(sku, n);
}
}
interface StockCommand {
readonly label: string;
execute(): void;
undo(): void;
}
class Receive implements StockCommand {
constructor(private inv: Inventory, private sku: string, private n: number) {}
get label() { return `receive ${this.n} ${this.sku}`; }
execute() { this.inv.set(this.sku, this.inv.qty(this.sku) + this.n); }
undo() { this.inv.set(this.sku, this.inv.qty(this.sku) - this.n); }
}
class Pick implements StockCommand {
constructor(private inv: Inventory, private sku: string, private n: number) {}
get label() { return `pick ${this.n} ${this.sku}`; }
execute() {
const onHand = this.inv.qty(this.sku);
if (onHand < this.n) throw new Error(`${this.label}: only ${onHand} on hand`);
this.inv.set(this.sku, onHand - this.n);
}
undo() { this.inv.set(this.sku, this.inv.qty(this.sku) + this.n); }
}
// Not reversible by arithmetic, so it captures the old value when it runs.
class CycleCount implements StockCommand {
private before = 0;
constructor(private inv: Inventory, private sku: string, private counted: number) {}
get label() { return `count ${this.sku} = ${this.counted}`; }
execute() {
this.before = this.inv.qty(this.sku);
this.inv.set(this.sku, this.counted);
}
undo() { this.inv.set(this.sku, this.before); }
}
class StockLedger {
private history: StockCommand[] = [];
readonly log: string[] = [];
run(cmd: StockCommand): void {
cmd.execute(); // throws before anything is recorded
this.history.push(cmd);
this.log.push(`do ${cmd.label}`);
}
undoLast(): void {
const cmd = this.history.pop();
if (!cmd) return;
cmd.undo();
this.log.push(`undo ${cmd.label}`);
}
}
const inv = new Inventory();
const ledger = new StockLedger();
ledger.run(new Receive(inv, "SKU-7", 40));
ledger.run(new Pick(inv, "SKU-7", 12));
try {
ledger.run(new Pick(inv, "SKU-7", 500));
} catch (e) {
console.log("refused:", (e as Error).message);
}
ledger.run(new CycleCount(inv, "SKU-7", 25));
ledger.undoLast();
console.log(inv.qty("SKU-7")); // 28
console.log(ledger.log.join("\n"));Trade-offs
Gains
- Undo, redo, queuing, retry and audit logging all fall out of one abstraction.
- Triggers are decoupled from receivers: one command serves a button, a shortcut and an API route.
- Commands compose into macros and transactions.
- Each command is testable as an
executethenundoround trip.
Costs
- One class per operation. A large app can accumulate hundreds of thin command types.
- Undo is only as correct as each command's inverse, and inverses drift when the receiver changes.
- Commands holding live receivers can't be persisted or sent across processes without a separate message format.
- Undo history holds references to receivers and captured state, which can keep memory alive.
Relations
- Memento stores whole state snapshots. Command stores the delta. Combine them when an operation has no cheap inverse.
- Strategy also puts behavior in an object, but to choose how to compute something. Command chooses what to do and when.
- Chain of Responsibility can pass command objects along its handlers.
- Mediator and Observer can dispatch commands between components without direct references.
- Prototype helps when a command must be copied into the history before it is run again.
Agent checklist
- There is a concrete need for undo, queuing, logging, retry or replay. Otherwise pass a function.
- Commands capture undo state in
execute, not in the constructor. - The invoker records a command only after it succeeds.
- Every command has an
executethenundoround-trip test that compares receiver state before and after. - If commands cross a process boundary, they are plain serializable data, not objects holding receivers.
See also
- MementoCapture an object's internal state in an opaque snapshot so it can be restored later without exposing that state to the code holding the snapshot.
- StrategyPut each interchangeable algorithm behind one interface so the caller picks a behavior at runtime without branching on it.
- Chain of ResponsibilityPass a request along a chain of handlers until one decides it, so the sender never knows which handler acts or how many exist.
- MediatorRoute interactions between a group of components through one coordinator so they stop referencing each other and the rules live in one place.
- ObserverLet an object announce its changes to a list of subscribers it does not know, so new reactions plug in without editing the source of the change.
- PrototypeCreate new objects by copying a configured existing instance, so callers get a ready-made variant without knowing its class or setup.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.