Encapsulating a request as an object to enable undo/redo and queuing — built through a text editor's undo history, and why Command is more than Strategy with a fancier name.
Published September 23, 2026
A plain method call happens and is gone — there's nothing left to inspect, queue, delay, or undo afterward. Command wraps a request (an action plus the data it needs) into an object, so the request itself becomes a first-class thing you can store, pass around, and reverse.
interface Command {
void execute();
void undo();
}
class TextDocument {
private final StringBuilder content = new StringBuilder();
void insert(int pos, String text) { content.insert(pos, text); }
void delete(int pos, int length) { content.delete(pos, pos + length); }
String getText() { return content.toString(); }
}
class InsertCommand implements Command {
private final TextDocument doc;
private final int position;
private final String text;
InsertCommand(TextDocument doc, int position, String text) {
this.doc = doc; this.position = position; this.text = text;
}
public void execute() { doc.insert(position, text); }
public void undo() { doc.delete(position, text.length()); } // the exact inverse of execute()
}
class EditorHistory {
private final Deque<Command> undoStack = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
void executeCommand(Command cmd) {
cmd.execute();
undoStack.push(cmd);
redoStack.clear(); // a fresh action invalidates any previously-undone redo history
}
void undo() {
if (undoStack.isEmpty()) return;
Command cmd = undoStack.pop();
cmd.undo();
redoStack.push(cmd);
}
void redo() {
if (redoStack.isEmpty()) return;
Command cmd = redoStack.pop();
cmd.execute();
undoStack.push(cmd);
}
}
Every executed command is pushed onto undoStack after running. Undo pops the most recent one and calls its undo() — moving it to redoStack so redo can re-execute it later. The redoStack.clear() on a fresh action is the detail easy to miss: once you undo twice and then type something new, the two undone actions are no longer a valid "redo" path — they'd redo into a document state that no longer makes sense given the new edit.
Strategy (see Strategy Pattern) wraps an interchangeable algorithm — the point is picking which implementation runs, with no concept of "undo" or "when." Command wraps a request, specifically so it can be queued, delayed, logged, or reversed — the "when" (execute now? later? as part of a batch?) and the "undo" are exactly what Command adds that Strategy doesn't model at all. A PricingStrategy has no meaningful "undo"; an InsertCommand does, by design.
The basic history stack supports undo. A usable editor also needs redo, clears the redo stack when a new command is executed, groups several commands into one undoable step, and merges rapid keystrokes, so undo doesn't remove one character at a time:
interface Command {
void execute();
void undo();
default boolean mergeWith(Command next) { return false; } // optional coalescing
}
final class History {
private final Deque<Command> undoStack = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
private final int limit;
History(int limit) { this.limit = limit; }
void run(Command c) {
c.execute();
redoStack.clear(); // a new action invalidates the redo branch
Command last = undoStack.peek();
if (last != null && last.mergeWith(c)) return; // typed "h","e","l","l","o" → one undo step
undoStack.push(c);
if (undoStack.size() > limit) undoStack.removeLast(); // bound the memory used by history
}
void undo() {
Command c = undoStack.poll();
if (c == null) return;
c.undo();
redoStack.push(c);
}
void redo() {
Command c = redoStack.poll();
if (c == null) return;
c.execute();
undoStack.push(c);
}
}
Macro commands (Composite) make a multi-step operation undoable as one unit:
final class MacroCommand implements Command {
private final List<Command> steps;
MacroCommand(List<Command> steps) { this.steps = List.copyOf(steps); }
public void execute() { steps.forEach(Command::execute); }
public void undo() {
for (int i = steps.size() - 1; i >= 0; i--) steps.get(i).undo(); // reverse order
}
}
history.run(new MacroCommand(List.of(new ReplaceCommand(doc, "colour", "color"),
new ReplaceCommand(doc, "favour", "favor")))); // "Replace all" = one undo
Merging lets consecutive typing at adjacent positions coalesce:
final class InsertCommand implements Command {
private final TextDocument doc; private int position; private String text;
InsertCommand(TextDocument doc, int position, String text) { this.doc = doc; this.position = position; this.text = text; }
public void execute() { doc.insert(position, text); }
public void undo() { doc.delete(position, text.length()); }
@Override public boolean mergeWith(Command next) {
if (next instanceof InsertCommand n && n.doc == doc && n.position == position + text.length()
&& !text.endsWith(" ")) { // stop merging at word boundaries
text = text + n.text; // n has already executed; just absorb it
return true;
}
return false;
}
}
Design notes:
Because a command is an object, it can be stored, queued, sent and replayed:
SendInvoiceEmail(invoiceId) command serialized onto a queue is executed later by a worker. That's the Command pattern across process boundaries.SaveCommand, so behaviour is defined once.A useful distinction in backend design: a command expresses intent ("PlaceOrder"), and may be rejected. An event records a fact ("OrderPlaced") that has already happened. Keeping the two separate makes systems easier to reason about.
Q: How would you implement a 'macro' — a single command that bundles several commands together?
A: A CompositeCommand implementing the same Command interface, holding a List<Command>, whose execute() runs each child in order and whose undo() undoes them in reverse order — this is Command combined with Composite (see Composite & Proxy), and the reverse-order undo is the detail that makes it correct: undoing must unwind history in the opposite order it was built.
Q: Why does InsertCommand.undo() call doc.delete() instead of storing a full document snapshot? A: Storing the minimal inverse operation (delete the exact range that was inserted) is far cheaper than snapshotting the whole document before every command — this matters a lot for a text editor where documents can be large and edits are frequent; snapshot-based undo only makes sense when computing a precise inverse operation is itself impractical.
Q: Can Command be used for something other than undo/redo? A: Yes — queuing (store commands in a queue and execute them later, e.g. a job queue), logging/auditing (every command object is a natural audit-log entry of what happened and when), and remote execution (serialize a command object and execute it on a different machine) are all common Command applications that have nothing to do with undo.
Q: What happens to undo() correctness if InsertCommand's undo() assumes nothing else modified the document between execute() and undo()? A: It breaks — if another command inserted or deleted text at an overlapping position in between, the stored position/length in InsertCommand.undo() would delete the wrong range. This is why undo/redo systems generally require commands to be undone in strict LIFO order relative to how they were executed (which the stack-based EditorHistory above enforces by construction) rather than allowing arbitrary out-of-order undo.
Q: Why must the redo stack be cleared when a new command runs? A: Redo re-applies commands that were undone from a specific document state. Once a new command changes the document, that state is gone, and re-applying the old commands would act on text positions and content that no longer match.
Q: How do macro commands keep undo correct? A: They execute their child commands in order, and undo them in reverse order, so each child's undo sees exactly the state its execute produced. The history treats the macro as one entry, so a single undo reverts the whole operation.
Q: What's the difference between a command and an event in backend systems? A: A command is a request to do something, and may be validated and rejected ("ReserveSeat"). An event states that something already happened, and can't be rejected, only reacted to ("SeatReserved"). Commands usually go to one handler, while events may have many subscribers.