Memento Pattern: State Snapshot and Restore

The Memento pattern solves a deceptively simple problem: how do you save and restore an object's state without tearing apart its encapsulation? You need this capability constantly—undo/redo in...

Key Insights

  • The Memento pattern captures an object’s internal state as an opaque snapshot, enabling undo/redo functionality without exposing implementation details or violating encapsulation.
  • Proper implementation requires careful attention to the narrow interface (what the Caretaker sees) versus the wide interface (what the Originator accesses), typically achieved through nested classes or module-level privacy.
  • Memory optimization through delta-based mementos and history depth limits is essential for production systems—naive implementations storing full state copies will crush your application under real-world usage.

Introduction to the Memento Pattern

The Memento pattern solves a deceptively simple problem: how do you save and restore an object’s state without tearing apart its encapsulation? You need this capability constantly—undo/redo in editors, checkpoint systems in games, transaction rollback in databases, and form state preservation in web applications.

The naive approach is to expose all internal state through getters and let external code manage snapshots. This works until your object’s internals change, breaking every piece of code that depended on that exposed structure. The Memento pattern provides a cleaner contract: the object itself creates opaque snapshots that only it can interpret.

Think of it like a sealed envelope. The Originator writes its state, seals the envelope, and hands it to a Caretaker for safekeeping. The Caretaker can hold onto multiple envelopes and return them on request, but it cannot read or modify what’s inside. When the Originator receives an envelope back, it opens it and restores its state.

Pattern Structure and Participants

The pattern involves three distinct roles with clear responsibilities:

Originator: The object whose state you want to capture. It creates mementos containing snapshots of its current state and can restore itself from a memento. This is your domain object—a document, game character, or form.

Memento: The snapshot object itself. It stores the Originator’s internal state and provides a narrow interface to the outside world (often just a marker interface or timestamp) while exposing its full contents only to the Originator.

Caretaker: The history manager. It requests mementos from the Originator, stores them (typically in a stack or list), and returns them when undo/redo is requested. Critically, it never examines or modifies memento contents.

// Basic structure demonstrating the three participants

interface Memento {
  readonly timestamp: Date;
  getName(): string;
}

class ConcreteMemento implements Memento {
  private state: string;
  readonly timestamp: Date;

  constructor(state: string) {
    this.state = state;
    this.timestamp = new Date();
  }

  getName(): string {
    return `${this.timestamp.toISOString()} - Snapshot`;
  }

  // Package-level access in practice - only Originator should call this
  getState(): string {
    return this.state;
  }
}

class Originator {
  private state: string = '';

  setState(state: string): void {
    this.state = state;
  }

  getState(): string {
    return this.state;
  }

  save(): Memento {
    return new ConcreteMemento(this.state);
  }

  restore(memento: ConcreteMemento): void {
    this.state = memento.getState();
  }
}

class Caretaker {
  private mementos: Memento[] = [];
  private originator: Originator;

  constructor(originator: Originator) {
    this.originator = originator;
  }

  backup(): void {
    this.mementos.push(this.originator.save());
  }

  undo(): void {
    const memento = this.mementos.pop();
    if (memento) {
      this.originator.restore(memento as ConcreteMemento);
    }
  }
}

Implementing Basic Undo/Redo Functionality

Let’s build something practical: a text editor with full undo/redo support. The key insight is maintaining two stacks—one for undo history and one for redo history. Every edit pushes to the undo stack and clears the redo stack. Undoing moves the current state to redo and pops from undo. Redoing does the reverse.

interface EditorMemento {
  readonly timestamp: Date;
}

class EditorSnapshot implements EditorMemento {
  readonly timestamp: Date;
  
  constructor(
    private readonly content: string,
    private readonly cursorPosition: number,
    private readonly selectionStart: number | null,
    private readonly selectionEnd: number | null
  ) {
    this.timestamp = new Date();
  }

  getContent(): string { return this.content; }
  getCursorPosition(): number { return this.cursorPosition; }
  getSelectionStart(): number | null { return this.selectionStart; }
  getSelectionEnd(): number | null { return this.selectionEnd; }
}

class TextEditor {
  private content: string = '';
  private cursorPosition: number = 0;
  private selectionStart: number | null = null;
  private selectionEnd: number | null = null;

  type(text: string): void {
    const before = this.content.slice(0, this.cursorPosition);
    const after = this.content.slice(this.cursorPosition);
    this.content = before + text + after;
    this.cursorPosition += text.length;
    this.clearSelection();
  }

  delete(count: number): void {
    const before = this.content.slice(0, this.cursorPosition);
    const after = this.content.slice(this.cursorPosition + count);
    this.content = before + after;
  }

  moveCursor(position: number): void {
    this.cursorPosition = Math.max(0, Math.min(position, this.content.length));
  }

  private clearSelection(): void {
    this.selectionStart = null;
    this.selectionEnd = null;
  }

  save(): EditorSnapshot {
    return new EditorSnapshot(
      this.content,
      this.cursorPosition,
      this.selectionStart,
      this.selectionEnd
    );
  }

  restore(snapshot: EditorSnapshot): void {
    this.content = snapshot.getContent();
    this.cursorPosition = snapshot.getCursorPosition();
    this.selectionStart = snapshot.getSelectionStart();
    this.selectionEnd = snapshot.getSelectionEnd();
  }

  getContent(): string { return this.content; }
}

class EditorHistory {
  private undoStack: EditorSnapshot[] = [];
  private redoStack: EditorSnapshot[] = [];
  private editor: TextEditor;
  private maxHistory: number;

  constructor(editor: TextEditor, maxHistory: number = 100) {
    this.editor = editor;
    this.maxHistory = maxHistory;
  }

  saveState(): void {
    this.undoStack.push(this.editor.save());
    this.redoStack = []; // Clear redo on new action
    
    // Enforce history limit
    if (this.undoStack.length > this.maxHistory) {
      this.undoStack.shift();
    }
  }

  undo(): boolean {
    const snapshot = this.undoStack.pop();
    if (!snapshot) return false;

    this.redoStack.push(this.editor.save());
    this.editor.restore(snapshot);
    return true;
  }

  redo(): boolean {
    const snapshot = this.redoStack.pop();
    if (!snapshot) return false;

    this.undoStack.push(this.editor.save());
    this.editor.restore(snapshot);
    return true;
  }

  canUndo(): boolean { return this.undoStack.length > 0; }
  canRedo(): boolean { return this.redoStack.length > 0; }
}

Encapsulation Considerations

The code above has a problem: EditorSnapshot.getContent() is public. Any code with a reference to the snapshot can read its internals. In TypeScript, we lack true access modifiers, but we can use closures and symbols to approximate proper encapsulation.

// Using closures for true encapsulation
const MEMENTO_KEY = Symbol('memento-access');

interface OpaqueMemento {
  readonly timestamp: Date;
  readonly name: string;
}

class SecureEditor {
  private content: string = '';
  private cursorPosition: number = 0;

  type(text: string): void {
    this.content = this.content.slice(0, this.cursorPosition) + 
                   text + 
                   this.content.slice(this.cursorPosition);
    this.cursorPosition += text.length;
  }

  save(): OpaqueMemento {
    // Capture state in closure - inaccessible to external code
    const capturedContent = this.content;
    const capturedCursor = this.cursorPosition;
    const capturedTimestamp = new Date();

    return {
      timestamp: capturedTimestamp,
      name: `Snapshot at ${capturedTimestamp.toISOString()}`,
      
      // Hidden restore function accessible only via symbol
      [MEMENTO_KEY]: {
        content: capturedContent,
        cursor: capturedCursor
      }
    };
  }

  restore(memento: OpaqueMemento): void {
    const internal = (memento as any)[MEMENTO_KEY];
    if (!internal) {
      throw new Error('Invalid memento');
    }
    this.content = internal.content;
    this.cursorPosition = internal.cursor;
  }

  getContent(): string { return this.content; }
}

In Java, you’d use a private inner class. In C++, friend classes. The principle remains: the Caretaker should have no way to inspect or modify memento contents.

Memory and Performance Optimization

Storing complete state snapshots works for small objects. For a document editor with megabytes of text, storing 100 complete copies is absurd. Delta-based mementos store only what changed between snapshots.

interface DeltaMemento {
  readonly timestamp: Date;
  readonly baseIndex: number; // Reference to base snapshot
}

interface Change {
  type: 'insert' | 'delete';
  position: number;
  content: string;
}

class DeltaSnapshot implements DeltaMemento {
  readonly timestamp: Date;
  readonly baseIndex: number;

  constructor(
    private readonly changes: Change[],
    baseIndex: number
  ) {
    this.timestamp = new Date();
    this.baseIndex = baseIndex;
  }

  getChanges(): Change[] { return [...this.changes]; }
}

class BaseSnapshot {
  constructor(
    private readonly content: string,
    private readonly cursorPosition: number
  ) {}

  getContent(): string { return this.content; }
  getCursorPosition(): number { return this.cursorPosition; }
}

class OptimizedEditorHistory {
  private baseSnapshots: BaseSnapshot[] = [];
  private deltas: DeltaSnapshot[] = [];
  private pendingChanges: Change[] = [];
  private editor: TextEditor;
  private deltaThreshold: number = 10;

  constructor(editor: TextEditor) {
    this.editor = editor;
    // Create initial base snapshot
    this.baseSnapshots.push(
      new BaseSnapshot(editor.getContent(), 0)
    );
  }

  recordChange(change: Change): void {
    this.pendingChanges.push(change);
  }

  checkpoint(): void {
    if (this.pendingChanges.length === 0) return;

    // Create new base snapshot periodically to limit delta chains
    if (this.deltas.length >= this.deltaThreshold) {
      this.baseSnapshots.push(
        new BaseSnapshot(this.editor.getContent(), 0)
      );
      this.deltas = [];
    }

    this.deltas.push(
      new DeltaSnapshot(
        [...this.pendingChanges],
        this.baseSnapshots.length - 1
      )
    );
    this.pendingChanges = [];
  }

  restore(steps: number): void {
    // Walk back through deltas, applying inverse operations
    // or reconstruct from nearest base snapshot
    const targetDeltaIndex = Math.max(0, this.deltas.length - steps);
    const baseIndex = this.deltas[targetDeltaIndex]?.baseIndex ?? 0;
    const base = this.baseSnapshots[baseIndex];
    
    // Reconstruct state by applying deltas from base
    let content = base.getContent();
    for (let i = 0; i <= targetDeltaIndex; i++) {
      if (this.deltas[i].baseIndex === baseIndex) {
        content = this.applyChanges(content, this.deltas[i].getChanges());
      }
    }
  }

  private applyChanges(content: string, changes: Change[]): string {
    for (const change of changes) {
      if (change.type === 'insert') {
        content = content.slice(0, change.position) + 
                  change.content + 
                  content.slice(change.position);
      } else {
        content = content.slice(0, change.position) + 
                  content.slice(change.position + change.content.length);
      }
    }
    return content;
  }
}

Real-World Applications

Game Save Systems: RPGs snapshot player position, inventory, quest progress, and world state. The Memento pattern keeps save/load logic cleanly separated from game mechanics. Consider compression and serialization for disk persistence.

Form State Preservation: Single-page applications can snapshot form state before navigation, restoring if the user returns. This prevents data loss without server round-trips.

Database Transactions: The pattern conceptually underlies transaction logs. Each transaction creates a memento of affected rows; rollback restores from these snapshots.

Command Pattern Comparison: Command pattern stores operations with their inverse; Memento stores state directly. Use Command when operations are easily reversible (toggle a boolean). Use Memento when computing inverses is complex or impossible (freeform text editing).

Summary and Best Practices

The Memento pattern provides clean state management when you need snapshot and restore capabilities. Keep these principles in mind:

Keep mementos immutable. Once created, a snapshot should never change. This prevents subtle bugs where shared references cause unexpected state mutations.

Enforce encapsulation strictly. The Caretaker must not access memento internals. Use language features (inner classes, symbols, closures) to make violations impossible, not just discouraged.

Limit history depth. Unbounded history will eventually exhaust memory. Set reasonable limits and consider delta compression for large state objects.

Consider serialization. For persistence across sessions, mementos need serialization. Design your memento structure with this in mind from the start—retrofitting serialization onto complex object graphs is painful.

Know when to use alternatives. For simple toggleable operations, Command pattern is lighter weight. For complex state machines, consider dedicated state management libraries. Memento shines when state is complex but snapshot semantics are straightforward.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.