Skip to article
ALGORITHMICSPatterns
Patterns6 min read

Command

A request as an object — and the undo stack you get almost for free.


Add undo to an editor.

The instinct is to snapshot the document before every change and keep the snapshots. For a text editor with a large document that is megabytes per keystroke, and it tells you nothing about what changed — so you cannot show “Undo Bold”, only “Undo”.

The pattern

Make each action an object that knows how to do itself and how to take itself back.

interface Command {
label: string;
execute(): void;
undo(): void;
}
class SetBold implements Command {
label = 'Bold';
private previous = false;
constructor(private readonly range: Range) {}
execute() {
this.previous = this.range.bold; // remember just enough to reverse it
this.range.bold = true;
}
undo() {
this.range.bold = this.previous;
}
}

The history is then a list of these, and the whole undo system is two stacks:

class History {
private done: Command[] = [];
private undone: Command[] = [];
run(command: Command) {
command.execute();
this.done.push(command);
this.undone = []; // a new action invalidates the redo branch
}
undo() { const c = this.done.pop(); if (c) { c.undo(); this.undone.push(c); } }
redo() { const c = this.undone.pop(); if (c) { c.execute(); this.done.push(c); } }
}

Try it — note what happens to the redo stack when you act after undoing:

·

history (undo stack)
empty
undone (redo stack)
empty

Each command stores its own reversal

SetBold records previous during execute, not during construction. That matters: the same command object might be re-executed by redo, at which point the state to restore may be different.

This is the real design work in Command, and it has a name — every command must capture exactly enough state to reverse itself, and no more. Store too little and undo is wrong; store the whole document and you are back to snapshots.

What else falls out of it

Once a request is an object, other things become easy that were not:

Queue it. A command can be executed later, or on another thread. Every job queue payload is a serialised command.

Log it. Write commands to a file as they run and you can replay them to reconstruct state. That is event sourcing, and it is the same idea a database’s write-ahead log uses.

Send it. A serialisable command can cross a network. Multiplayer editors send commands, not documents, because commands are small and can be transformed against each other.

Batch it. A MacroCommand holding a list of commands, executing them in order and undoing them in reverse, is itself a Command — that is Composite applied to behaviour.

class Macro implements Command {
constructor(private readonly parts: Command[]) {}
execute() { for (const p of this.parts) p.execute(); }
undo() { for (const p of [...this.parts].reverse()) p.undo(); }
}

The reverse() is not decoration. Undoing in the same order would restore earlier state on top of later state.

The lighter version

If you do not need undo, a command is a function, and the pattern is Strategy:

button.onClick = () => document.setBold(range);

The class earns its place specifically when you need the pair — do and undo — or when the request must outlive the call, by being queued, logged or sent.