How do you implement undo and redo using the Command pattern, and what state must each command object capture to be reversible?
answer
- undo() + two stacks
- new command clears redo
- inverse op vs memento snapshot
- capture oldValue / generated IDs at execute time
- composite undoes children in reverse order
basics
~20 sAdd an undo() method next to execute(). Each executed command is pushed onto an undo stack. Undo pops it, calls undo(), and pushes it onto a redo stack. The command must remember whatever it needs to restore the previous state.
solid answer
~50 sExtend the command interface with `undo()` and keep two stacks: undo and redo. `execute()` runs the command and pushes it onto the undo stack while clearing the redo stack; `undo` pops from undo, calls `undo()`, pushes onto redo; `redo` pops from redo, re-executes, pushes back onto undo. The crucial design decision is what each command stores. Two strategies: **inverse-operation undo**, where the command computes the reverse (insert ↔ delete) — cheap in memory but only valid when the operation is truly invertible and no other actor mutated the state in between; and **memento undo**, where `execute()` snapshots the affected state before mutating and `undo()` restores it — robust, works for lossy operations like "apply filter" or "clear formatting", but costs memory. Commands must also capture their arguments at construction/execution time (position, previous value, generated IDs) rather than re-reading live state, or undo will apply against a moved target.
code
typescript · 17 linesclass SetProperty implements Command {
private oldValue: unknown // captured, not recomputed
constructor(private target: any, private key: string, private newValue: unknown) {}
execute() {
this.oldValue = this.target[this.key] // snapshot before mutating
this.target[this.key] = this.newValue
}
undo() { this.target[this.key] = this.oldValue }
}
class History {
private undoStack: Command[] = []; private redoStack: Command[] = []
run(c: Command) { c.execute(); this.undoStack.push(c); this.redoStack.length = 0 }
undo() { const c = this.undoStack.pop(); if (c) { c.undo(); this.redoStack.push(c) } }
redo() { const c = this.redoStack.pop(); if (c) { c.execute(); this.undoStack.push(c) } }
}go deeper
Describe the undo() method and the two stacks, and show one concrete reversible example such as insert ↔ delete.
Explain what state must be captured (old value, generated IDs), contrast inverse-operation undo with memento snapshots, and note that a new command clears the redo stack.
Add coalescing, composite commands undone in reverse order, bounded history, atomicity of execute(), and the limits of inverse undo when other actors mutate state concurrently.
Connect the undo stack to transaction logs, compensating transactions, and event sourcing; discuss persistence and versioning of serialized commands, history-as-tree vs stack, and why collaborative editing needs OT/CRDT rather than naive inverse undo.
## Why Command is the natural home for undo Undo requires you to know *what was done* after it was done. A direct method call leaves no trace. A command object is a durable record of the request, so it is the obvious place to attach the knowledge of how to reverse it. ## The interface and the two stacks ``` interface Command { execute(); undo() } class History { undoStack = []; redoStack = [] run(cmd) { cmd.execute(); undoStack.push(cmd); redoStack.clear() } undo() { if (undoStack.empty) return cmd = undoStack.pop(); cmd.undo(); redoStack.push(cmd) } redo() { if (redoStack.empty) return cmd = redoStack.pop(); cmd.execute(); undoStack.push(cmd) } } ``` Three rules that interviewers look for: 1. **A new command clears the redo stack.** Once you branch away from the undone future, that future is unreachable — keeping it would let redo apply against a state it was never recorded on. (Editors that keep it must model history as a tree, not a stack.) 2. **Undo/redo must be exactly symmetric.** `execute → undo → execute` must land in the same state as a single `execute`. If `execute()` allocates something non-deterministic — a new ID, a timestamp, a random seed — it must capture it on the first run and reuse it on redo, otherwise redo silently produces different data. 3. **Only reversible, state-mutating commands belong on the history.** Queries, navigation, and side effects on the outside world (sending an email, charging a card) either should not enter the stack or need an explicit compensating action. ## What state must the command capture? This is the real question. Two strategies: ### A. Inverse operation (logical undo) The command stores the minimal data needed to compute the reverse: - `InsertText(text, pos)` → undo = `delete(pos, text.length)` - `SetProperty(obj, key, newValue)` → must also store `oldValue`, captured during `execute()` - `MoveShape(dx, dy)` → undo = move by `(-dx, -dy)` Cheap in memory, precise, and serializes well. But it is only sound when: - the operation is genuinely invertible (no information lost), and - nothing else mutated the affected state between execute and undo — otherwise the inverse is applied to a different world. In a single-user editor with a strict linear history this holds by construction; in a multi-user or background-mutating system it does not, and you need operational transformation / CRDTs or coarse locking. Lossy operations break this outright: "convert image to grayscale" or "clear all formatting" cannot be inverted from the operation description alone. ### B. Memento (state snapshot) `execute()` captures a snapshot of the affected state (the Memento pattern) before mutating; `undo()` restores it. Works for anything, including lossy operations, and is far simpler to get right. The cost is memory and copy time, mitigated by: - snapshotting only the *affected region* rather than the whole document, - persistent/immutable data structures with structural sharing, so a "snapshot" is an O(1) reference, - coalescing (see below) and capping history depth. Most real editors mix both: inverse for cheap, obviously invertible edits; memento for anything complex. ## Practical refinements - **Coalescing / merging.** Typing 200 characters should not be 200 undo steps. Commands expose something like `canMergeWith(previous)` and the history merges consecutive same-kind commands within a time window. - **Composite / macro commands.** A single user action that touches several receivers is wrapped in one command containing children; its `undo()` reverses the children in **reverse order**. This keeps the user-visible undo granularity aligned with user intent. - **Bounded history.** Keep the last N commands or a memory budget; drop the oldest. Note that dropping the oldest is safe with mementos but subtly unsafe with pure inverse-based undo if commands depend on earlier ones — usually fine because undo is strictly LIFO. - **Transient vs persistent history.** In-memory stacks die with the process. Persisting history means the commands must be serializable (see the log/replay dimension of the pattern) and versioned. - **Selective/out-of-order undo** ("undo just that one change from 5 steps ago") breaks the stack model entirely — it requires either re-applying the whole history minus one command, or a conflict-aware model. Say this out loud in an interview; it shows you know the limits. ## Failure handling If `execute()` throws halfway, the command must not be pushed onto the undo stack in a partially applied state. Either make `execute()` atomic (do all the validation and computation first, mutate last) or have it roll back its own partial work before rethrowing. For composites, undo the children that already succeeded before propagating the error. ## Relation to bigger systems An undo stack is a small, in-memory instance of the same idea as a database transaction log or an event-sourced stream: an ordered record of state-changing requests that can be replayed forward or compensated backward. Undo via inverse operations corresponds to compensating transactions; undo via memento corresponds to snapshot-and-restore.
- Why must performing a new command clear the redo stack?The redo entries were recorded against a state that no longer exists once history branches. Re-applying them would mutate a different world and likely corrupt state. Systems that want to keep both branches must model history as a tree rather than a stack.
- How would you undo a command that sent an email or charged a credit card?You can't reverse it technically — you need a compensating action (send a retraction, issue a refund), and it must be modeled explicitly as its own command. Best practice is to keep externally visible side effects out of the undo stack entirely, or to defer them until the change is committed past the undo horizon.
- Typing produces one command per keystroke and floods the undo history. What do you do?Coalesce: let commands declare whether they can merge with the immediately preceding one (same kind, same target, within a short time/position window) and merge them into a single history entry, so one undo removes a whole typed word or burst.
A ledger with correcting entries. To reverse a bookkeeping mistake you either post the exact opposite entry (inverse operation) or restore last night's photocopy of the page (memento). Either way you need the ledger — the ordered record of what was done — which is exactly what the command history is.
saying these in an interview costs you the question
- Storing only the new value in a 'set property' command and expecting undo to work — the old value must be captured at execute time.
- Letting a new command sit on top of a non-empty redo stack instead of clearing it.
- Assuming every operation is invertible; lossy operations (grayscale, clear formatting) need a memento snapshot.
- Re-reading live state inside undo() instead of using values captured during execute() — the target may have moved.
- Pushing a command onto the undo stack even though execute() failed partway, leaving history describing a state that never existed.
- Claiming undo/redo needs no extra memory — either you store inverses or you store snapshots; something is always retained.