Command Pattern

Encapsulate a request as an object so you can parameterise, queue, log, and undo it. The basis for almost every editor's history.

Pattern Intermediate
9 min read
pattern behavioral command undo queue

What it is#

The Command pattern encapsulates a request as an object — turning a method call into a value you can pass around, store, queue, log, and reverse. The Gang of Four gave four roles: Command (the interface with execute), ConcreteCommand (a specific request with the parameters bound in), Receiver (the object that actually does the work), and Invoker (the thing that pushes commands through, often a button, a queue, or a history).

The shape is what makes things possible. Once a request is a value, you can:

  • Hand it to a queue and execute it later.
  • Hand it to a thread pool and execute it elsewhere.
  • Push it onto a stack and replay it (macros).
  • Pair it with a reverse and pop it off the stack (undo).
  • Persist it to disk and replay it on restart (write-ahead logs, event sourcing).

Every editor’s Ctrl+Z is this pattern. Every job queue is this pattern. Java’s Runnable is this pattern with the receiver and the parameters fused into the closure.

Class structure#

┌────────────────────┐ ┌────────────────────┐
│ Invoker │◇───────►│ Command │
├────────────────────┤ * ├────────────────────┤
│ history │ │ execute() │
│ submit(cmd) │ │ undo() │
│ undo() │ └─────────┬──────────┘
└────────────────────┘ │
┌───────────┴───────────┐
│ │
┌──────────┴──────┐ ┌──────────┴──────┐
│ ConcreteCmdA │ │ ConcreteCmdB │
├─────────────────┤ ├─────────────────┤
│ receiver │ │ receiver │
│ params │ │ params │
│ execute() │ │ execute() │
│ undo() │ │ undo() │
└────────┬────────┘ └────────┬────────┘
│ │
└──────────►┌───────────┴────────┐
│ Receiver │
├────────────────────┤
│ doActualWork(...) │
└────────────────────┘

The ConcreteCommand holds a reference to the Receiver and the parameters needed to invoke it. The Invoker only knows the Command interface.

When to use it#

Reach for Command when any of these are in the requirements:

  • Undo / redo. The textbook driver. Every operation that changes state needs an inverse.
  • Queueing. Requests must be deferred — queued, scheduled, retried, dispatched to a worker pool.
  • Logging / audit. Every change should be replayable from a journal.
  • Macros. A user records a sequence of operations and plays them back.
  • Decoupling the caller from the receiver. The button does not need to know what it does; it just executes its command.

Common scenarios:

  • Editors — text, image, code, vector. Every keystroke or click that mutates state is a command.
  • Job / task queuesExecutorService.submit(Runnable) is the pattern at scale.
  • Transactional systems — write-ahead logs, event-sourcing systems, redo logs in databases.
  • Remote procedure calls — serialize a command, ship it across the wire, execute on the other side.
  • GUI toolkits — button → action mapping, keyboard shortcut → action mapping.

When not to use it:

  • When the request is a simple synchronous method call that never needs to be deferred, queued, or undone. Wrapping a one-line call in a Command class is ceremony.
  • When the parameters change with every call in ways that defeat reuse. The command object has to capture parameters; if you build a new one every time, you may as well call the method.
  • When you cannot define a meaningful inverse but the design pretends you can. A “delete user” command whose undo actually restores soft-deleted rows is fine; one whose undo cannot recover the data is a lie waiting to bite.

How it works#

A text-editor example with an undo stack. Two commands — Insert and Delete — both reversible. The invoker is an Editor that pushes each command onto a history stack and pops it for undo.

import java.util.ArrayDeque;
import java.util.Deque;
public interface Command {
void execute();
void undo();
}
public final class TextBuffer {
private final StringBuilder text = new StringBuilder();
public void insert(int pos, String s) {
text.insert(pos, s);
}
public String delete(int pos, int length) {
String removed = text.substring(pos, pos + length);
text.delete(pos, pos + length);
return removed;
}
public String snapshot() { return text.toString(); }
}
public final class InsertCommand implements Command {
private final TextBuffer buffer;
private final int position;
private final String text;
public InsertCommand(TextBuffer buffer, int position, String text) {
this.buffer = buffer;
this.position = position;
this.text = text;
}
@Override public void execute() {
buffer.insert(position, text);
}
@Override public void undo() {
buffer.delete(position, text.length());
}
}
public final class DeleteCommand implements Command {
private final TextBuffer buffer;
private final int position;
private final int length;
private String removed; // captured at execute time, used at undo time
public DeleteCommand(TextBuffer buffer, int position, int length) {
this.buffer = buffer;
this.position = position;
this.length = length;
}
@Override public void execute() {
removed = buffer.delete(position, length);
}
@Override public void undo() {
if (removed == null) throw new IllegalStateException("undo before execute");
buffer.insert(position, removed);
}
}
public final class Editor {
private final TextBuffer buffer = new TextBuffer();
private final Deque<Command> history = new ArrayDeque<>();
private final Deque<Command> redo = new ArrayDeque<>();
public void run(Command c) {
c.execute();
history.push(c);
redo.clear(); // new branch invalidates redo
}
public void undo() {
if (history.isEmpty()) return;
Command c = history.pop();
c.undo();
redo.push(c);
}
public void redo() {
if (redo.isEmpty()) return;
Command c = redo.pop();
c.execute();
history.push(c);
}
public String text() { return buffer.snapshot(); }
public TextBuffer buffer() { return buffer; }
}
// Wiring:
// Editor ed = new Editor();
// ed.run(new InsertCommand(ed.buffer(), 0, "Hello, "));
// ed.run(new InsertCommand(ed.buffer(), 7, "World!"));
// ed.text(); // "Hello, World!"
// ed.undo(); // "Hello, "
// ed.undo(); // ""
// ed.redo(); // "Hello, "

Five details earn their keep:

  • DeleteCommand captures state at execute time. The text to restore is not known when the command is built — only when it runs. Capturing it on execute and using it on undo is the standard idiom for non-trivial inverses.
  • InsertCommand is its own inverse-friendly twin. The undo of insert is delete; the undo of delete is insert. The pair is symmetric, which is why these are the textbook examples.
  • Redo clears on new commands. Once the user does something new after undoing, the alternative future is gone. This matches every editor users have ever used.
  • History is a Deque. Push to record, pop to undo. The same data structure powers undo and redo with no extra cleverness.
  • The invoker depends only on Command. Editor.run does not know there are insert and delete commands. New commands plug in without editing the editor.

Variants#

VariantMechanismWhen it fits
Pure execute (no undo)Command has only execute().Job queues, event handlers, fire-and-forget actions. Runnable is this.
Reversible (execute + undo)The two-method form above.Editors, transactional UIs, anywhere “undo” is a feature.
Composite / macro commandA MacroCommand holds a list of commands and executes them in order; undo runs them in reverse.Macro recording, batch operations, multi-step wizards.
Persistable commandCommands serialize to JSON / Protobuf for write-ahead logs or RPC.Event sourcing, distributed task queues, save-and-replay testing.
Lambda commandsA Runnable or Callable<V> captures the work inline.When the command is stateless and one-shot — the class boilerplate is pure ceremony.

Runnable and Callable<V> are Command without the undo half. ExecutorService.submit(Runnable) is the invoker; the thread pool is the dispatcher; the work is the command. The JDK uses the pattern by another name on every concurrency surface.

Example systems#

  • Editor history — Photoshop, VS Code, IntelliJ, Figma. Every operation is a command; the history panel is the stack.
  • Database write-ahead logs — every commit is a serialized command; replay rebuilds the database. The first paragraph of every WAL paper is the Command pattern with extra steps.
  • Event-sourcing systems — domain events are commands; the current state is the fold over the history.
  • Job queues — Sidekiq, Celery, AWS SQS workers, ExecutorService. The queued payload is a serialized command; the worker executes it.
  • GUI action systems — Eclipse / IntelliJ commands, browser keyboard-shortcut handlers, every menu item in macOS’ AppKit (@selector(saveDocument:) is the receiver-and-method-as-value).
  • Distributed transactions — saga steps and their compensating actions are commands and their inverses.

Trade-offs#

What you gain:

  • Operations as first-class values. You can store, schedule, log, ship over the wire, and replay them. Every one of those abilities falls out for free.
  • Undo and redo come naturally. Pair an inverse with each command and the editor history is one stack and one method.
  • Decoupling. The invoker depends on Command; the receiver is bound inside the concrete command. The button never knew what it did.
  • Composition. Macros are commands made of commands. The pattern composes with itself.
  • Open for extension. New commands plug in without editing the invoker — Open-Closed Principle, again.

What you pay:

  • Class explosion. A serious editor has hundreds of commands. Keeping them organized requires discipline (per-feature folders, naming conventions, factories).
  • Undo is hard. A truly correct inverse is rare in practice. Side effects (network calls, file system, IPC) make undo either a lie or a full transaction system.
  • State capture is subtle. Commands that mutate composite state (cut a tree node, reformat a paragraph) must capture enough at execute time to fully restore — partial captures produce ghost bugs years later.
  • Macros change semantics. A macro that re-executes “delete current selection” three times does not delete three items unless the macro captures the selections, not the action. Macros usually need parameter-binding rules of their own.
  • Persisted commands have version drift. If the command schema changes, the old logs become unreadable. Event-sourcing systems pay this tax constantly.

Direct call

editor.insert(0, "Hello");

One line. No object. No undo. No queue. No log. Adequate when none of those things matter.

Command

editor.run(new InsertCommand(buffer, 0, "Hello"));

More keystrokes; the request is now a value. You can undo it, queue it, log it, ship it. Adequate when any of those things matter.

  • Strategy Pattern — both turn behavior into objects, but a Strategy is one of many alternative algorithms; a Command is one request bound to its parameters. A strategy is reusable; a command is single-use (or single-trajectory).
  • Observer Pattern — Observer tells you when something happened; Command captures what to do about it. A notification often carries a command for the observer to execute later.
  • State Pattern — Commands often drive state transitions. The State object can refuse a Command that is not valid in this state, giving you a free validation layer.
  • Memento pattern (not covered in this workbook) — Memento captures a full snapshot for undo; Command captures the delta needed to reverse one operation. Memento is heavier and easier; Command is lighter and harder.
  • Composite Pattern — MacroCommand is a Composite of commands. The two patterns combine without friction.
  • Open Closed Principle (OCP) — Command is OCP for “we keep adding new things the user can do.”
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.