Snapshot Undo Is Underrated. The Whole 'Command Pattern Is Correct' Argument Misses the Point.
TLDR
Traditional software architecture tutorials label the Command Pattern (execute() / undo()) as the only valid approach for building undo systems. For modern canvas and schematic editors, full-state snapshot undo ({ before, after }) is significantly faster to implement, immune to edge-case command drift, and far easier to maintain. Paired with focus-time state capture, snapshot undo eliminates per-feature command classes.
| Undo Architecture | Implementation Overhead | Maintenance Cost | Primary Failure Mode |
|---|---|---|---|
| Command Pattern | High (Custom do/undo per feature) | High (Command classes for every tool) | Logic drift in inverse commands |
| Full Snapshot Undo | Low (Uniform JSON state pairs) | Zero (Handled by serializer) | Late before-state capture |
Problem statement: the overhead of the command pattern
The Command Pattern requires developers to write bidirectional transformation logic for every editor feature:
- Move Operation: Store coordinates and apply inverse vectors.
- Delete Operation: Serialize deleted DOM subtrees and restore them in place.
- Property Change: Track previous vs. new values for every input field.
Technical failure mode: subtle inverse command drift
In complex diagramming engines, writing exact inverse commands is error-prone:
- Unbalanced Operations: A
GroupCommand.undo()might restore elements, but fail to re-establish wire port connectivity listeners. - Command Avalanche: Adding a single new tool requires creating dedicated command classes, test cases, and stack serializers.
The fix: timed snapshot state pairs
Snapshot undo replaces custom command objects with uniform state snapshot pairs:
// Uniform Snapshot History Manager
class SnapshotUndoEngine {
constructor() {
this.historyStack = [];
}
push(label, beforeState, afterState) { // Ignore identical snapshots if (JSON.stringify(beforeState) === JSON.stringify(afterState)) return; this.historyStack.push({ label, before: beforeState, after: afterState }); }
undo() { const entry = this.historyStack.pop(); if (entry) restoreState(entry.before); } }
By hooking before-state capture into user interaction entry points (focus for forms, pointerdown for drags), snapshot undo handles every current and future feature without requiring custom command classes.
Rule of thumb: Prefer full-state snapshot undo over the Command Pattern for web-based schema editors. Timed state serialization eliminates per-feature inverse command overhead.