Memento

Advanced · 10 min read · ▶ live playground · ✦ checkpoint

A text editor adds checkpoints by returning { text, cursor } to its history manager. Undo works, but the history manager can now edit the text, invent an invalid cursor, or grow dependent on every future private field.

The Memento pattern captures an object's internal state in an opaque snapshot and lets that object restore the snapshot later. It preserves encapsulation, but full snapshots can consume substantial memory and may copy far more data than an inverse operation needs.

Before: history reaches through the object's boundary

The first undo design exposes the editor state so another object can save and restore it:

interface EditorState {
  text: string;
  cursor: number;
}
 
class TextEditor {
  public text = "";
  public cursor = 0;
}
 
class EditorHistory {
  private snapshots: EditorState[] = [];
 
  public save(editor: TextEditor): void {
    this.snapshots.push({
      text: editor.text,
      cursor: editor.cursor,
    });
  }
 
  public restore(editor: TextEditor): void {
    const state = this.snapshots.pop();
    if (state === undefined) return;
 
    editor.text = state.text;
    editor.cursor = state.cursor;
  }
}

EditorHistory now knows which fields define a valid editor. Adding a selection range means changing both classes. Nothing stops history from restoring cursor: 900 into a five-character document.

Making the fields read-only to ordinary clients does not solve restoration; history still needs a privileged route around the editor's invariant. The editor itself should create and consume its snapshots.

After: the originator owns snapshot meaning

The originator is the object whose state is captured and restored. TextEditor is the originator because only it understands which fields form a valid editor state.

A memento is an opaque snapshot produced by an originator. Opaque means its contents are hidden from the object storing it. The caretaker is that storage object: it keeps mementos and chooses when to return one, but it neither reads nor edits their contents.

class diagram

TextEditor creates snapshots that EditorHistory stores opaquely

TextEditor creates snapshots that EditorHistory stores opaquely
  1. 1TextEditor is the originator; it alone decides which internal fields belong in a snapshot.
  2. 2save() produces an opaque Memento, and restore() interprets that same snapshot.
  3. 3EditorHistory is the caretaker; it owns snapshot order without reading editor state.

The ownership edge does not grant EditorHistory access to the snapshot's fields. It says the caretaker retains memento objects. TextEditor remains the only class that knows how to turn one into valid live state.

This TypeScript version uses a module-private symbol as the snapshot key. Code outside the module can hold the Memento contract, but it cannot name the key that unlocks editor state:

const mementoBrand: unique symbol = Symbol("memento");
const snapshotKey: unique symbol = Symbol("editor snapshot");
 
interface Memento {
  readonly [mementoBrand]: true;
}
 
interface EditorState {
  text: string;
  cursor: number;
}
 
class EditorMemento implements Memento {
  public readonly [mementoBrand] = true as const;
  public readonly [snapshotKey]: EditorState;
 
  public constructor(state: EditorState) {
    this[snapshotKey] = { ...state };
  }
}
 
class TextEditor {
  private text = "";
  private cursor = 0;
 
  public type(value: string): void {
    this.text =
      this.text.slice(0, this.cursor) +
      value +
      this.text.slice(this.cursor);
    this.cursor += value.length;
  }
 
  public save(): Memento {
    return new EditorMemento({
      text: this.text,
      cursor: this.cursor,
    });
  }
 
  public restore(memento: Memento): void {
    if (!(memento instanceof EditorMemento)) {
      throw new Error("Memento belongs to another originator");
    }
 
    const snapshot = memento[snapshotKey];
    this.text = snapshot.text;
    this.cursor = snapshot.cursor;
  }
 
  public get content(): string {
    return this.text;
  }
}
 
class EditorHistory {
  private snapshots: Memento[] = [];
 
  public push(memento: Memento): void {
    this.snapshots.push(memento);
  }
 
  public pop(): Memento {
    const memento = this.snapshots.pop();
 
    if (memento === undefined) {
      throw new Error("No checkpoint available");
    }
 
    return memento;
  }
}
 
const editor = new TextEditor();
const checkpoints = new EditorHistory();
 
editor.type("Design");
checkpoints.push(editor.save());
editor.type(" patterns");
 
console.log(`current: ${editor.content}`);
editor.restore(checkpoints.pop());
console.log(`restored: ${editor.content}`);

current: Design patterns restored: Design

The caretaker stores a value whose public type exposes no text or cursor. On restore, the editor verifies that the memento came from its own implementation, reads the hidden snapshot, and restores both fields together.

In a real module, snapshotKey and EditorMemento stay unexported. Callers receive only Memento, while the exported originator provides save() and restore(). The language boundary then supports the pattern's promise instead of relying on comments.

Memento and Command undo can cooperate

The Command lesson made each request responsible for reversal. A command can calculate a small inverse, such as “remove the nine characters I inserted.” It can also save a memento before execute() and give that snapshot back to the receiver during undo().

That combination is useful when an operation changes several private fields and deriving the exact inverse is risky. The command owns when to capture and restore; the originator owns what the captured state contains. The invoker still sees an opaque command rather than editor internals.

Snapshot timing matters. Saving after execution records the new state, not the state undo needs. A command that uses a memento normally captures it immediately before its first mutation.

When NOT to use it

Large or frequent snapshots are memory-heavy. Saving a 50 MB document before every keystroke can retain gigabytes of mostly repeated data. Structural sharing lets snapshots reuse unchanged data instead of copying it; compressed or periodic checkpoints can reduce storage too.

An inverse command can be much cheaper. If inserting ten characters can be undone by removing ten characters at one position, storing that position and text length costs less than copying the whole document. Memento trades cheap restoration and strong encapsulation for snapshot storage and copying work.

External side effects also resist snapshots. Restoring an order object does not unsend an email or refund a captured payment. Use domain-specific compensating operations for those effects, and do not imply that an in-memory snapshot rewinds the outside world.

Checkpoint

Answer all three to mark this lesson complete

Memento gives one object a safe path back to an earlier state. Next, Chain of Responsibility gives one request a path forward through a configurable line of handlers.

+50 XP on completion