Command pattern
Open in the interactive app: practice, quiz, mock interview →In one minute
- A command turns a request ("insert
hiat position 4", "refund order 81") into an object or value that can be stored, queued, logged, sent over a network, retried, undone and replayed. The code that triggers an action no longer has to be the code that performs it, or run at the same time. - The classic interface is
execute()plus, for undoable commands,undo(). An undo stack and a redo stack of commands give an editor its history; a queue of commands gives a job system its work. - In most modern code a command is a plain function or closure (a Go
func(), a JavaScript callback) or a serialisable record ({"type": "refund", "order": 81}) handled by a dispatcher. Choose the record form when commands must cross a process boundary or survive a restart. - The costs are memory for history (every undoable command keeps what it needs to reverse itself) and correctness work: undo must be the exact inverse, and queued commands need idempotency because delivery is often at least once.
- Agents tend to implement undo by snapshotting the whole state on every action. That works in a demo and fails on a 20 MB document. Ask for inverse operations and a bounded history.
How it works
The problem
A browser-based text editor started with toolbar buttons and keyboard handlers that edit the document directly: the Bold button changes the selection, Backspace removes a character, Paste inserts a block. Then users ask for undo, a command palette, keyboard macros and collaborative editing. Each of these needs the same thing: to treat "what the user did" as data. Undo needs to know how to reverse the last action. The palette needs to list actions by name. Macros need to record a sequence and play it back. Collaboration needs to send actions to other clients.
The command pattern gives each action an object:
class Insert:
def __init__(self, pos, text):
self.pos, self.text = pos, text
def execute(self, doc):
doc.text = doc.text[:self.pos] + self.text + doc.text[self.pos:]
def undo(self, doc):
doc.text = doc.text[:self.pos] + doc.text[self.pos + len(self.text):]Buttons, keys and the palette now create commands and hand them to a History object, which executes them and keeps them on an undo stack. The invoker (button, shortcut, macro player) knows nothing about how editing works; the receiver (the document) knows nothing about buttons.
The four roles
The Gang of Four name four participants. The command holds a request and its parameters. The receiver is the object that does the work (the document, the bank account). The invoker triggers commands and may store them (the toolbar, a scheduler, the history). The client creates commands and wires them to receivers. In practice the invoker is where the interesting behaviour lives: history, queueing, retrying, logging.
Undo and redo
A linear history needs two stacks. Executing a command pushes it on the undo stack and clears the redo stack, because the redo entries were computed against a state that no longer exists. Undo pops a command, calls its undo, and pushes it on the redo stack. Redo does the reverse. Each command must store exactly the data it needs to reverse itself: Insert needs the position and text; Delete must save the text it removed when it executes, because the original command only knew a position and a length.
Two refinements matter in real editors. Coalescing: typing "hello" should be one undo step, not five, so consecutive adjacent inserts merge into one command. Emacs groups runs of typed characters this way, and ProseMirror's history starts a new group only when an edit is not adjacent to the previous one or arrives more than 500 ms later. Bounding: history is capped by count or bytes, dropping the oldest entries.
Commands as values
When commands must outlive the process, they become records: a type name plus arguments, serialised as JSON or protobuf. Task queues store exactly that (a Celery task message carries the task name and its arguments), CQRS systems accept commands such as PlaceOrder that a handler validates and executes, VS Code registers every action under a string ID so menus, keybindings and extensions call executeCommand("editor.action.formatDocument"), and Redis's append-only file is a log of write commands replayed on restart. The task scheduler lesson stores actions this way, and the tic-tac-toe lesson uses moves as commands for undo; this lesson focuses on the inverse operations and history management those designs rely on.
Command versus event
A command is a request that may be rejected ("reserve seat 14C"); an event is a fact that already happened ("seat 14C reserved"). Keeping the two apart makes logs honest: replaying events rebuilds state without rerunning validation, while replaying commands would rerun business rules that may have changed.
Reviewing agent-written code
Asked to "add undo", an agent commonly deep-copies the whole document before each action and pushes the copy on a stack. Memory grows with document size times history length, and anything holding a reference to the old document breaks. Steer it to commands with inverses, a redo stack cleared on new commands, a bounded history, and tests that run execute then undo and compare with the original state. For queued commands, check that each carries an idempotency key and that handlers tolerate duplicates.
The same shape elsewhere
Commands address the ordering and multi-step consistency shapes: once an action is a value, you can sequence it, log it before applying it, and compensate for it. A database's write-ahead log records changes before applying them, so it can redo them after a crash; a saga stores a compensating command for each step so it can undo a partly completed business transaction; Git commits are recorded changes that git revert inverts with a new commit.
Down the stack
Layer 1The contract
An undoable command promises that undo applied immediately after execute restores the receiver to exactly the prior state, and that execute either fully applies or fails without changing anything (so a failed command is never recorded). A history promises linear semantics: undo reverses the most recent recorded command, redo is available only until a new command executes, and the history holds at most its configured limit. A queued command promises at-least-once or exactly-once delivery; at-least-once is far more common, so handlers must be idempotent, usually by deduplicating on a command ID.
Layer 2The mechanism
Emacs keeps undo information in buffer-undo-list, a list of entries such as (BEG . END) for inserted text and (TEXT . POSITION) for deleted text, separated by nil boundaries that mark where one command ends; undo walks back to the previous boundary and applies each inverse. ProseMirror represents edits as steps; each step can produce its inverse against the document it was applied to, and the history plugin stores inverted steps along with position maps so earlier entries can be rebased when collaborators change the document. Redis appends each write command to the AOF in the same RESP encoding clients send, and the appendfsync setting (always, everysec or no) decides how often the file is flushed with fsync. Go's os/exec.Cmd is a command in the literal sense: a struct describing a process to run, built first and started later with Run or Start.
Layer 3The machine
Each command is a heap allocation of a few dozen bytes plus its payload, and each execute is one indirect call: an itab lookup in Go, a vtable call in Java or C++, a prototype-chain lookup in JavaScript that V8's inline caches turn into a fixed offset when the call site sees few command classes. A Go closure used as a command allocates its captured variables once when it is created. None of that matters for user actions arriving at tens per second.
History memory does matter. A snapshot undo on a 20 MB document with a 500-step history needs up to 10 GB. Inverse commands store only what changed: an insert of 5 characters costs its position and those bytes, and a delete stores the removed text. Coalescing typed characters cuts entry counts by roughly an order of magnitude. String edits themselves can be the hidden cost: splicing a flat string is O(n) per edit, which is why editors store text as ropes or piece tables, where an insert touches O(log n) nodes and undo can reference unchanged pieces instead of copying them.
For concurrency, a single-consumer command queue is the simplest correct design: one goroutine or thread applies commands in order, so the receiver needs no locks, which is how Redis executes commands and how many actor systems work. Multiple producers enqueue through a channel or lock-free queue. When several users edit at once, commands must be transformed against each other (operational transformation) or replaced by CRDT operations, because an Insert(pos=10) means something different after another user's insert at position 3.
Why it's built this way
- Actions triggered in one place often need to run later, elsewhere, again or in reverse, so they must be data rather than direct calls.
- Reversing an action needs information captured when it ran, so each command stores its own inverse data.
- Snapshots cost memory proportional to the whole state, so storing inverses scales with the size of changes instead.
- Redo entries depend on the state they were made against, so a new command must invalidate them.
- Commands crossing processes can be delivered twice, so handlers deduplicate by ID rather than trusting the transport.
Diagram
stateDiagram-v2 [*] --> Idle Idle --> Executed: execute(cmd) ok, push undo, clear redo Idle --> Idle: execute(cmd) fails, nothing recorded Executed --> Executed: execute(next), merge if adjacent insert Executed --> Undone: undo(), pop undo, push redo Undone --> Executed: redo(), pop redo, push undo Undone --> Executed: execute(new), redo cleared Undone --> Undone: undo() again
Worked example
A web document editor holds documents up to 2 MB of text, and users make about 3 edit commands a second while typing. The first undo implementation snapshotted the whole document per keystroke with a 1,000-step history: up to 2 GB per open tab in the worst case and about 40 MB typically, and the browser killed tabs on low-memory phones.
The team replaced snapshots with Insert and Delete commands storing positions and changed text, and coalesced adjacent inserts. A typical 30-minute session of 5,400 keystrokes became about 600 history entries averaging 40 bytes of text plus around 60 bytes of object overhead, roughly 60 KB in total. A cap of 10,000 entries or 5 MB, whichever comes first, bounds the worst case.
The tests that caught the most bugs were property tests: generate a random document and a random sequence of valid commands, execute them all, undo them all, and compare with the original; then redo them all and compare with the final state. They found two bugs in the first week: Delete at the end of a document stored one character too few, and merging an insert after an undo corrupted the redo stack.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Direct method calls | Simplest; no extra objects | No undo, queueing, logging or replay | Actions that never need any of those |
| Closures as commands | Concise; captures context | Not serialisable; inverse must be written by hand | In-process callbacks, retries, deferred work |
Command objects with undo | Precise undo; small memory | Every command needs a correct inverse | Editors, design tools, admin consoles |
| Snapshot (memento) undo | Trivially correct | Memory scales with state size | Small state, or periodic checkpoints combined with commands |
| Serialisable command records | Queue, log, send and replay | Schema versioning; idempotency needed | Job queues, CQRS, replication, audit |
Pitfalls in production
- Undo that is not an exact inverse, such as a delete that does not save what it removed. Fix: capture inverse data during
executeand test execute then undo round trips with generated inputs. - Redo stack left intact after a new command, so redo applies against the wrong state. Fix: clear redo on every new command.
- Unbounded history consuming memory in long sessions. Fix: cap by count and bytes, and coalesce small adjacent edits.
- Failed commands recorded in history, so undo reverses something that never happened. Fix: validate before mutating and record only after success.
- Queued commands executed twice after a redelivery, charging a card twice. Fix: give each command an ID and deduplicate in the handler.
- Serialised commands whose schema changes break replay of old logs. Fix: version command types and keep handlers for old versions until the log is compacted.
Leadership lens
- Decide early whether actions need undo, audit or replay. Retrofitting commands into an app built on direct mutations touches every feature.
- Require an inverse and a round-trip test for every new undoable command; it is a cheap rule that prevents subtle data loss.
- For queued commands, make idempotency keys part of the API contract, not an implementation detail. Duplicate delivery is a normal operating condition.
- Keep commands and events distinct in logs and naming. Mixing them makes incident timelines and replays hard to trust.
Check yourself
Why must executing a new command clear the redo stack?
Show answer
Redo entries were recorded against a state that the new command has changed, so reapplying them could corrupt the document or apply edits at wrong positions.
Why does a
Deletecommand need to store data duringexecutethat it was not constructed with?Show answer
It is built with a position and length, but undo must reinsert the exact text removed, which is only known when it runs against the document.
When is snapshot undo better than inverse commands?
Show answer
When the state is small or inverses are hard to write correctly; many systems combine periodic snapshots with commands to bound replay time.
Why do queued command handlers need idempotency?
Show answer
Most queues deliver at least once, so a crash after executing but before acknowledging leads to redelivery; deduplicating by command ID makes the second execution harmless.
Watch
- Command Pattern (Christopher Okhravi): invoker, receiver and command worked through, including undo.
- 10 Design Patterns Explained in 10 Minutes (Fireship): command alongside the other behavioural patterns, in JavaScript.
Free resources
- Undo (GNU Emacs Lisp Reference Manual): the entry types and boundaries of a real undo list.
- prosemirror-history (GitHub): an editor history built from inverted steps, with grouping by adjacency and time.
- Implementing Undo History (Redux docs): past, present and future stacks in a state-container setting.
- Redis persistence (Redis docs): the append-only file as a durable log of commands.
- CQRS (Martin Fowler): commands as first-class messages in application architecture, and when that is overkill.
- Command pattern (Refactoring Guru): the four roles with diagrams and examples.