For implementing undo, when would you store state snapshots (Memento) versus storing reversible operations (Command), and how are the two often combined?
answer
- state vs operation: restore old, or invert change
- memory ∝ state × depth vs ∝ ops
- lossy/non-deterministic ops need a scoped memento
- hybrid: command carries small snapshot
- snapshot every N + replay log (checkpointing)
basics
~20 sSnapshots save the whole state and restore it wholesale — simple and always correct, but costly when the state is large. Reversible operations save just the action and how to invert it — cheap in memory, but every operation needs a correct inverse. Many systems use commands and let a command carry a small memento for the part it cannot invert.
solid answer
~60 sMemento-based undo captures the originator's state before a change and restores it later; correctness is easy (restore is one code path) and it handles non-invertible operations, but memory scales with state size times history depth, and restoring a whole document to undo one keystroke is coarse. Command-based undo stores the operation plus the data needed to invert it (undo/redo methods); memory scales with the number of operations, undo is fine-grained, and it naturally supports macros, logging, and replay — but every command must implement a correct inverse, which is error-prone for lossy operations (delete, flatten, normalize, random or time-dependent effects). The pragmatic combination: commands drive the history, and any command whose effect cannot be computed backwards captures a small memento of just the affected region before executing (a 'partial' or scoped memento). A third variant, common in editors and functional systems, is persistent immutable state: each edit yields a new version sharing structure with the old one, so full snapshots become cheap and undo is just holding an old root reference.
go deeper
Contrast the two ideas in one line each — save the old state versus save how to reverse it — and note snapshots use more memory.
Add the invertibility test, give a concrete lossy operation, and mention bounded history stacks and redo handling.
Argue the hybrid: commands with scoped mementos, checkpoint-plus-log for long histories, and the memory/latency numbers that drive the choice.
Position it as checkpointing strategy across databases, event sourcing, and collaborative editing (operations as the wire format), plus persistent data structures collapsing snapshot cost and the boundary where compensating transactions are the only real undo.
## Two ways to go backwards Undo needs one of two things: **the old state**, or **a way to reverse the change**. **Memento / state-based undo.** Before mutating, ask the object for a snapshot; push it on a stack; on undo, restore it. - *Correctness*: trivially right. There is one restore path, and it works for any operation, including ones that destroy information. - *Cost*: memory proportional to `sizeof(state) × historyDepth`. Snapshotting a 50 MB document per keystroke is untenable. - *Granularity*: coarse. You restore everything, which can also clobber unrelated concurrent changes. **Command / operation-based undo.** Encapsulate each user action as an object with `execute()` and `undo()`; the history stores commands. - *Cost*: memory proportional to the number of operations and the parameters each carries — usually tiny. - *Extras*: commands are also queueable, loggable, replayable, transmittable (great for collaborative editing and for audit), and composable into macros. - *Risk*: every command needs a **correct inverse**. That is hard or impossible when the operation is *lossy* (delete, crop, merge, round, deduplicate) or *non-deterministic* (uses randomness, current time, external I/O). A subtly wrong inverse produces state drift that only shows up after several undo/redo cycles. ## The invertibility test Ask: *given the post-state and the command's parameters alone, can I recompute the pre-state exactly?* - `insert("abc", at 5)` → yes, delete 3 chars at 5. - `setBold(range)` → not quite: which characters were already bold? Capture the previous formatting. - `deleteSelection()` → no, the text is gone. Capture it. - `applyFilter(random seed)` → no, unless the seed and the exact pixels are captured. Everything that fails the test needs to carry state — a **scoped memento** of just the affected slice. ## The hybrid (what real editors do) Commands own the history; each command captures the minimal memento it needs: ``` class DeleteSelection : Command { private var saved: TextRun? = null // scoped memento fun execute(doc) { saved = doc.cut(range) } fun undo(doc) { doc.insert(range.start, saved!!) } } ``` You get fine granularity and bounded memory, while lossy operations stay correct. This also keeps the caretaker uniform: it holds commands, not a mix of types. ## Periodic snapshots over an operation log When the history is long, replaying operations from the beginning gets slow. The standard fix — used by databases (checkpoints + write-ahead log), event-sourced systems (aggregate snapshots + event stream), and version control — is **snapshot + log**: keep a full memento every N operations, and replay only the operations after the nearest snapshot. Here Memento is purely a performance optimization over a command/event log, and it is the answer to "replay is the source of truth but it's too slow". ## Persistent data structures: the third option If the state is an immutable/persistent structure (a tree where an update returns a new root sharing all unchanged subtrees), then a "full snapshot" is just a pointer to the old root, and it costs O(changed path), not O(state). Undo becomes: keep the old root. This collapses the memory objection to Memento almost entirely, and is why functional-style editors and things like immutable virtual DOM histories favour snapshots. The trade-off moves to allocation pressure, garbage-collection load, and the discipline of keeping *everything* in the persistent structure. ## Decision checklist | Situation | Prefer | |---|---| | Small state, few undo steps, want it done today | Memento snapshots | | Large state, high-frequency edits | Command with scoped mementos | | Operations are lossy or non-deterministic | Must capture state (memento), at least scoped | | Need audit log, replay, collaboration, or remoting of edits | Command (operations are the artifact) | | Long log, slow replay | Command log + periodic memento snapshots | | State already immutable/persistent | Snapshots — they are nearly free | | Side effects escape the object (files, network, payments) | Neither undoes them; need compensating actions | ## The escape-hatch caveat Both mechanisms only govern in-process state. If an operation sent an email, charged a card, or committed a row visible to others, no snapshot or inverse call retracts it. You need explicit compensating transactions, and the UI should not promise undo for such steps.
- Give an operation that cannot be undone by a pure inverse function and say what you would capture instead.Deleting a selection, or applying a lossy filter/rounding: the original bytes are gone, so no inverse can recompute them. Capture a scoped memento of exactly the affected region (the deleted run, the original pixel tile) inside the command, and reinsert it on undo.
- Why do event-sourced systems still store snapshots if the event log is the source of truth?Purely for speed. Rebuilding an aggregate by replaying thousands of events is slow, so they persist a snapshot every N events and replay only the tail. The snapshot is a derived cache and can be discarded and regenerated from the log.
- How does redo fit into each approach?With commands, redo re-executes the popped command, so you keep two stacks. With snapshots, you must also snapshot the state you are leaving before restoring, otherwise the current state is lost and redo has nothing to return to.
saying these in an interview costs you the question
- Claiming every operation can be inverted, ignoring lossy and non-deterministic ones.
- Snapshotting the entire document on every keystroke and being surprised by memory or latency.
- Treating Memento and Command as mutually exclusive rather than routinely combined.
- Forgetting that undo must snapshot the current state before restoring, or redo becomes impossible.
- Assuming undo can retract external side effects such as sent messages or committed payments.
- Assuming replaying a command log always reproduces the same state when commands read the clock, randomness, or external services.