Engineering Journal
Schema Editor
Schema Editor

Snapshot Undo Is Underrated. The Whole 'Command Pattern Is Correct' Argument Misses the Point.

2026-06-04

TLDR

The standard advice is that command pattern undo is correct and snapshot undo is a hack for prototypes. This is wrong for most interactive editors. Snapshot undo is simpler to implement correctly, produces fewer edge cases, and the only real failure mode (capturing before-state too late) is easy to prevent once you understand it.


What the Industry Does

Command pattern undo is the textbook answer. Every operation is encoded as a command object with execute() and undo() methods. The undo stack is a list of command objects. Pressing Ctrl+Z calls undo() on the last command.

This is elegant. It is also a significant amount of work. You need to encode both directions of every operation: move requires storing the delta and reversing it; delete requires storing the deleted element and re-inserting it; property change requires storing the old value and the new value per field. Every new feature requires a new command class.

Why It Fails for This Problem Class

Command pattern undo breaks down when operations are not discrete. A user dragging an element through 200 positions before releasing does not produce 200 commands. It produces one "Move" command with a start and end position. But determining "start" requires knowing where the element was before the drag began.

A user editing a text field character by character does not produce N character commands. It produces one "Edit" command with an old value and a new value. But "old value" has to be captured before typing started.

In both cases you are back to the same problem as snapshot undo: capture state at the start of the interaction, commit a single entry at the end. The command pattern does not eliminate the before-state timing problem. It just names the before and after values differently.

The Better Approach

Snapshot undo captures full editor state at the start and end of each discrete interaction. The history stack is an array of {before, after} pairs. Undo applies before. Redo applies after.

The only implementation requirement: capture before-state at interaction start, not at commit time. For input fields this is focus. For drags this is pointerdown. This is one line of code per interaction type.

// The entire undo implementation for an input field
let before = null;
field.addEventListener('focus', () => { before = captureState(); });
field.addEventListener('blur', () => {
    if (!before) return;
    applyValue(field.value);
    pushHistory(before, captureState());
    before = null;
});

No command class. No encode/decode logic. No per-field change tracking. The state serialization handles every operation uniformly.

What You Give Up

Snapshot undo stores complete state snapshots. For a large diagram with hundreds of elements, each snapshot can be several hundred kilobytes. A 100-step history is tens of megabytes. Command pattern undo stores only the changed fields, typically a few bytes per operation.

If your editor handles diagrams with thousands of elements and users are expected to maintain 100+ undo steps, snapshot undo will cause memory pressure. For most interactive diagram editors, diagrams are small enough that this is not a real constraint.

Snapshot undo also cannot merge adjacent operations (two consecutive text edits into one undo step) without explicit logic. Command pattern merging is built into the command interface. For editors where merge matters, command pattern has a structural advantage.

When the Common Pattern Is Right

Use command pattern undo when: state snapshots are expensive (very large graphs, 3D scenes), undo merge is important for user experience, or the operations are well-defined and discrete enough that encoding both directions is not a maintenance burden.

For diagram editors, property panels, and canvas tools where the operation set grows continuously, snapshot undo with focus-time capture is faster to implement and harder to get wrong.

Read this post in the full Engineering Journal →