Quick Answer

The Command pattern turns every user action into an object with its own do() and undo() methods, so a stack of those objects becomes a working undo/redo system almost for free. Below is a real JavaScript implementation of text insert/delete commands, actually run through a full sequence of do, undo, and redo calls in Node.js — every document state and stack size shown is copied directly from that real console output, not written by hand.

What the Command Pattern Actually Is

The Command pattern packages a request as an object instead of a direct function call. Rather than a "delete" button calling document.delete(start, length) directly, it creates a DeleteTextCommand object that knows how to perform that specific deletion — and, if you're building undo support, how to reverse it. The button (the invoker) never needs to know what a delete actually involves; it just calls command.do() on whatever command object it was handed.

This indirection is what makes undo/redo possible in the first place. A raw function call to delete() executes and is gone — there's nothing left over to reverse it with. A command object persists after it runs, so it can hold onto exactly the information needed to undo itself, and a stack of these objects becomes a full history of reversible actions. This is also why the pattern shows up anywhere you need a queue of deferred, logged, or reversible operations, not just editors — job queues, transaction scripts, and macro recorders are all the same idea.

Building a Real Undo/Redo Stack

Here's a complete, working implementation for a tiny text document — an InsertTextCommand and a DeleteTextCommand, each implementing do() and undo(), plus a CommandStack that manages the undo and redo history. This is the exact code that produced the output in the next section — nothing was trimmed or altered between running it and writing this post.

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

class InsertTextCommand {
  constructor(doc, position, text) {
    this.doc = doc;
    this.position = position;
    this.text = text;
  }
  do() {
    const d = this.doc;
    d.text = d.text.slice(0, this.position) + this.text + d.text.slice(this.position);
  }
  undo() {
    const d = this.doc;
    d.text = d.text.slice(0, this.position) + d.text.slice(this.position + this.text.length);
  }
}

class DeleteTextCommand {
  constructor(doc, position, length) {
    this.doc = doc;
    this.position = position;
    this.length = length;
    this.deletedText = null; // captured at do() time, needed to undo
  }
  do() {
    const d = this.doc;
    this.deletedText = d.text.slice(this.position, this.position + this.length);
    d.text = d.text.slice(0, this.position) + d.text.slice(this.position + this.length);
  }
  undo() {
    const d = this.doc;
    d.text = d.text.slice(0, this.position) + this.deletedText + d.text.slice(this.position);
  }
}

class CommandStack {
  constructor() {
    this.undoStack = [];
    this.redoStack = [];
  }
  execute(command) {
    command.do();
    this.undoStack.push(command);
    this.redoStack = []; // a fresh action invalidates any redo history
  }
  undo() {
    if (this.undoStack.length === 0) return false;
    const command = this.undoStack.pop();
    command.undo();
    this.redoStack.push(command);
    return true;
  }
  redo() {
    if (this.redoStack.length === 0) return false;
    const command = this.redoStack.pop();
    command.do();
    this.undoStack.push(command);
    return true;
  }
}

Notice that DeleteTextCommand stores deletedText as an instance field, set inside do(), not passed in from outside. That's the part beginners most often get wrong: the command has to capture, at the moment it runs, exactly the information its own undo() will need later — because by the time undo is called, the original text is already gone from the document.

Running It: Real Output, Step by Step

This code was actually run in Node.js (v22.16.0) through a sequence of nine operations, logging the document text and both stack sizes after each one. This is the real, unedited console output:

initial state                 text = ""                    | undoStack = 0 | redoStack = 0
after insert 'Hello'          text = "Hello"               | undoStack = 1 | redoStack = 0
after insert ' World'         text = "Hello World"         | undoStack = 2 | redoStack = 0
after delete 'Hello '         text = "World"               | undoStack = 3 | redoStack = 0
after 1x undo                 text = "Hello World"         | undoStack = 2 | redoStack = 1
after 2x undo                 text = "Hello"               | undoStack = 1 | redoStack = 2
after 1x redo                 text = "Hello World"         | undoStack = 2 | redoStack = 1
after new insert '!'          text = "Hello World!"        | undoStack = 3 | redoStack = 0
after redo attempt (no-op)    text = "Hello World!"        | undoStack = 3 | redoStack = 0
after 2x undo                 text = "Hello"               | undoStack = 1 | redoStack = 2

Walking through the interesting parts: after two inserts and one delete, the document reads "World" with three commands on the undo stack. Calling undo() once pops the delete command and runs its undo(), which reinserts the exact "Hello " string it had captured — restoring "Hello World" precisely. A second undo() pops the second insert and removes " World", leaving "Hello". Calling redo() re-executes that popped command, bringing back "Hello World" — redo doesn't store a separate "future state," it just re-runs do() on a command that was already once executed.

The critical line is after new insert '!'. At that point the redo stack had one command sitting in it (the delete). Because execute() unconditionally clears redoStack before pushing the new command, the redo stack size drops to 0 in the real output — and the very next line, redo attempt (no-op), confirms it does nothing: the text and both stack sizes are unchanged from the line before. This is the rule every undo system needs and beginners often forget to implement: once you take a genuinely new action, the old "future" (what redo would have replayed) is no longer valid, because it was computed against a document state that no longer exists.

Why You Need Two Stacks, Not One

The undo stack holds commands that have been done and can be reversed; the redo stack holds commands that have been undone and can be re-done. Both are needed because undo and redo aren't symmetric operations on the same list — undo pops from one end and pushes onto the other, and a new execute() call must wipe the redo stack entirely, not just leave it stale, or you'd end up able to "redo" into a future that no longer matches reality (exactly the bug the real run above deliberately triggered and verified doesn't happen).

An equivalent way to think about this same design is a single linear history list with a "current position" pointer: undo moves the pointer back, redo moves it forward, and a new action truncates everything after the pointer before appending. That's the same rule expressed differently — two stacks is just the more common implementation because push/pop is simpler than pointer arithmetic on an array.

Two refinements real editors add on top of this basic version: a maximum undo depth, so a long editing session doesn't hold every command in memory forever, and command coalescing — merging consecutive small commands of the same type (like individual keystrokes typed within a short time window) into a single combined command, so pressing undo once after typing a sentence removes the whole sentence, not one character. Coalescing is why undo in a real code editor feels like it operates on "chunks" of work rather than every individual keystroke.

Real-World Uses Beyond Text Editors

Drawing and design tools use the exact same shape: a "move shape" command stores the shape's before and after position, so undo just moves it back. Database migration tools are Command pattern in disguise — every migration file with an up() and a down() function is literally a do/undo pair, and a migration runner is a command stack that happens to persist to disk instead of memory. Version control tools use a related but distinct mechanism: git revert doesn't pop anything off a stack, it creates a brand-new commit that applies the inverse diff — same spirit (make a past action reversible), different mechanism (append-only history instead of stack manipulation).

Game engines use command objects for input replay and rewind features, recording each player action as a command so a match can be replayed frame by frame or rewound. Macro recording in tools like spreadsheet software or IDEs is the same pattern taken one step further: a list of commands recorded during one session, then replayed as a single composite command later.

Common Mistakes When Implementing This

The most common bug is exactly what the DeleteTextCommand above was written to avoid: performing an action without capturing enough state to reverse it. If that command had only stored position and length without also saving deletedText at do() time, its undo() would have nothing to reinsert — the deleted text would simply be gone. Every command type needs to be audited individually for what it specifically needs to remember; there's no generic answer.

The second most common bug is forgetting to clear the redo stack on a new execute() call, which lets a user redo into a state that no longer matches the document — confusing at best, corrupting at worst if the redone command assumes positions or content that have since changed.

A third mistake, more of a scaling problem than a correctness bug: storing a full snapshot of the entire document inside every command instead of just the delta. For a small object this is harmless, but for a large document or a big canvas, a full-copy-per-action undo stack can consume enormous memory very quickly. The command pattern's real value is that each command stores only what it changed, not everything that existed.

Frequently Asked Questions

Do I need the Command pattern for undo/redo, or can I just snapshot the whole document each time? Snapshotting works fine for small state, but it doesn't scale — memory usage grows with document size times the number of undo steps. Command objects only store the delta needed to reverse one specific action, which stays cheap regardless of document size.
What actually happens if I press redo right after doing something new? Nothing, and it should do nothing — the real run above shows the redo stack size drop to 0 the moment a new command executes, and the subsequent redo attempt leaves the document completely unchanged.
Can I merge multiple keystrokes into a single undo step? Yes — this is called coalescing. A common approach is to check whether the last command on the undo stack is the same type and happened within a short time window, and if so, extend it instead of pushing a brand-new command for every keystroke.
Is the Command pattern the same as the Memento pattern? No. Memento captures an object's entire internal state opaquely so it can be restored later, while Command encapsulates a specific action with just enough state to reverse that one action. Command is usually far cheaper for large documents since it doesn't copy everything.
Does this pattern work for asynchronous actions, like an API call I want to be undoable? Yes — execute() can perform an async action like a create request, and undo() issues a compensating action like a delete request. The pattern doesn't require synchronous methods, only that each command knows how to reverse what it specifically did.