Design Pattern

Command Pattern

The Command pattern turns a request - “delete this paragraph”, “deploy this build”, “move this shape” - into a first-class object. Once an action is an object you can store it, queue it, log it, send it across the network, replay it, or, most usefully, undo it. The pattern puts a clean seam between the code that decides something should happen and the code that actually does it.

If you have ever wired up a “Cmd+Z” handler, dispatched a Redux action, fired off a GraphQL mutation, or sent an event to a state machine, you have already used the pattern - whether or not anyone called it by name.


The canonical example: undo/redo in an editor

The reason the Command pattern earns its keep is undo. A text editor that mutates a document with raw method calls (doc.insert(...), doc.delete(...)) has nowhere to record what just happened, so it cannot reverse it. By wrapping each edit as a command object that knows how to do and undo itself, undo becomes a stack push.

Start with a minimal document:

class Document {
  constructor(text = "") {
    this.text = text;
  }
}

Every command implements the same tiny interface: execute(doc) performs the change and undo(doc) reverses it.

class InsertText {
  constructor(position, payload) {
    this.position = position;
    this.payload = payload;
  }
  execute(doc) {
    doc.text =
      doc.text.slice(0, this.position) +
      this.payload +
      doc.text.slice(this.position);
  }
  undo(doc) {
    doc.text =
      doc.text.slice(0, this.position) +
      doc.text.slice(this.position + this.payload.length);
  }
}

class DeleteRange {
  constructor(position, length) {
    this.position = position;
    this.length = length;
    this.removed = ""; // captured at execute() so undo can replay it
  }
  execute(doc) {
    this.removed = doc.text.slice(this.position, this.position + this.length);
    doc.text =
      doc.text.slice(0, this.position) +
      doc.text.slice(this.position + this.length);
  }
  undo(doc) {
    doc.text =
      doc.text.slice(0, this.position) + this.removed + doc.text.slice(this.position);
  }
}

The “commander” - sometimes called an invoker - holds two stacks and forwards calls to whichever command it is handed:

class History {
  constructor(doc) {
    this.doc = doc;
    this.past = [];
    this.future = [];
  }
  run(command) {
    command.execute(this.doc);
    this.past.push(command);
    this.future.length = 0; // any new edit invalidates the redo branch
  }
  undo() {
    const command = this.past.pop();
    if (!command) return;
    command.undo(this.doc);
    this.future.push(command);
  }
  redo() {
    const command = this.future.pop();
    if (!command) return;
    command.execute(this.doc);
    this.past.push(command);
  }
}

Now editing is just three method calls, but undo, redo, and edit history come for free:

const doc = new Document("Hello world");
const history = new History(doc);

history.run(new InsertText(5, ","));      // "Hello, world"
history.run(new DeleteRange(6, 6));        // "Hello,"
history.undo();                            // "Hello, world"
history.redo();                            // "Hello,"

Notice how the editor (History) knows nothing about what an edit does - it only knows that every command can execute and undo. New commands (paste, format, transform) drop in without touching the history class. That decoupling is the whole point of the pattern.


You’re probably using it already

The Command pattern shows up under a lot of names in modern JavaScript:

  • Redux actions. An action like { type: "todos/added", payload } is a serialized command. The reducer is the receiver; the store is the invoker. Action replay, time-travel debugging, and Redux DevTools are all standard consequences of the pattern.
  • GraphQL mutations. Each mutation is a named command with a payload; resolvers are the receivers. Persisted queries take this further by giving each command a stable ID on the wire.
  • CQRS and event sourcing. The “C” in CQRS is the Command pattern, formalized: writes are commands that produce events, which are then replayed to rebuild state.
  • State machines. In XState and similar libraries, events sent to a machine are commands; the transition table decides which receiver runs.
  • Job runners and CI/CD. Each step in a GitHub Actions workflow, each task in a queue (BullMQ, Sidekiq), each migration in a database tool - all serializable commands, executed by an invoker that handles retries, ordering, and logging.
  • Browser commands. document.execCommand was a literal implementation of this pattern (now deprecated in favor of more specific APIs). The newer EditContext API preserves the command-shaped seam.

Modern variants

A few small refinements show up often enough to call out:

Plain-object commands. Classes are not required. A discriminated-union object - { type: "insert", position, payload } - plus a switch statement in the invoker is the same pattern with less ceremony, and it serializes cleanly for network or storage.

Macro commands. Group a batch of commands into a single unit so that undo reverses the whole group. The recipe is a MacroCommand whose execute runs each child in order and whose undo runs them in reverse.

Optimistic commands. In offline-first apps, run the command locally, push it onto a pending queue, and reconcile when the server responds. CRDTs and frameworks like Replicache build on this exact shape.

Coalescing. Successive InsertText commands within the same keystroke burst can be merged before being pushed to the history stack, so one Cmd+Z reverses a word rather than a single character. Many editors expose this as command.merge(previous).


Command vs Strategy vs callback

These three patterns are often confused because all three pass behavior around as a value. The distinction is what the receiver is supposed to do with it:

PatternIntentWhen to reach for it
CommandCapture a request as an object that can be stored or replayedYou need undo/redo, queueing, logging, transactional rollback, or to send the operation across a network or process.
StrategyPick how a single operation is performed at runtimeYou have one job (sorting, compressing, routing) but multiple interchangeable algorithms.
CallbackHand a function to “call back” when something happensThe work is fire-and-forget and you don’t need to inspect, queue, or reverse it. Use this when you don’t need the rest.

If a plain function callback gives you everything you need, use it. Reach for Command only when you need the extra capabilities the object form unlocks.


Trade-offs

The Command pattern earns its complexity when at least one of these is true: you need undo/redo, the request must outlive the call (a queue, a log, a network hop), or you have many request shapes that must share a single invoker. If none of those apply, a direct method call is clearer and shorter.

Two pitfalls worth flagging. First, do not let commands hold large object graphs - they live as long as your history stack, which can be a memory problem in long-running editors. Capture the minimum data needed to undo. Second, beware “stale” undo: if external state changes between execute and undo, the original undo may no longer make sense. Editors solve this by snapshotting just the affected region (as DeleteRange.removed does above) rather than the whole document.


References