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.
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.