Behavioral Pattern (GoF)

Memento

Captures and externalizes an object's internal state without violating its encapsulation, allowing it to be restored to that state later — the foundation of undo via a complete snapshot.

Intent

Capture an object's internal state in a separate object (Memento), without exposing that state's implementation details to the outside world. The Memento can be stored and later used to restore the object to the captured state — implementing undo, checkpoints, transactions, and state history.

Cataloged by the GoF (1994) as a behavioral pattern, Memento preserves the Originator's encapsulation: whoever stores the Memento (the Caretaker) doesn't know what's inside it — it just keeps it and hands it back. Only the Originator knows the state's format and knows how to create and restore a Memento. This distinguishes the pattern from a simple public serialization of the state: the Memento's interface is opaque to any object other than the Originator that created it.

Problem

Continuing the text editor domain from the Command pattern: we want to implement undo. The naive approach is to publicly expose the editor's state so an external object can save it:

// Naive approach — violates encapsulation:
class Editor {
  content: string = '';   // public so the Caretaker can save it
  cursorPos: number = 0;  // public so the Caretaker can save it
  selection: [number, number] | null = null; // public
}

// The Caretaker accesses and knows the Editor's internals — strong coupling.
// If the Editor changes its fields, the Caretaker breaks.
const savedState = {
  content: editor.content,
  cursorPos: editor.cursorPos,
  selection: editor.selection,
};

The problem is twofold: the Caretaker (whoever stores the state) needs to know the internal details of the Editor (whoever owns the state), creating strong coupling. Any internal refactoring of the Editor — renaming a field, changing the selection's structure — breaks the Caretaker. Memento eliminates this: the Editor produces an opaque object with its state, and the Caretaker just stores it — without inspecting it.

Critical distinction: Memento vs Command with undo

Both patterns implement undo, but with different philosophies:

  • Command with undo: each Command encapsulates a specific action and knows how to revert it incrementally. An InsertTextCommand knows how to delete exactly the text it inserted. Undo is precise but requires each Command to capture enough state for its own reversal.
  • Memento: captures the complete state of the Originator in a snapshot. Undo restores the entire state — it doesn't undo a specific action, it returns the object to an earlier point in time. Simpler to implement for objects with rich, interdependent state, but more costly in memory (one snapshot per restoration point).

The two patterns are complementary: a Command can use Memento as its state-capture mechanism in its execute(), delegating the responsibility of "how to save the state" to the Originator itself.

Solution

Memento organizes the code into three participants:

  1. Originator: the object whose state we want to capture. It creates Mementos via saveState() and restores its state from a Memento via restoreState(memento). It's the only one that knows the Memento's internal content. E.g.: Editor with content, cursor position, and selection.
  2. Memento: an object that stores the snapshot of the Originator's state. Its interface toward the Caretaker is opaque — it doesn't expose getters for the internal state. Only the Originator accesses the content. In TypeScript, this can be modeled with an internal class or private fields with an access method restricted via a nominal type.
  3. Caretaker: manages the history of Mementos. It doesn't inspect or modify the Mementos' content — it just stores them in order and hands them back to the Originator when undo is requested. E.g.: History with a stack of Mementos and save() and undo() methods.

Structure

       Editor (Originator)
  ┌──────────────────────────────────────────────┐
  │ - content: string                            │
  │ - cursorPos: number                          │
  │ - selection: [number,number] | null          │
  │                                              │
  │ + saveState(): EditorSnapshot                │
  │   → creates a Memento with a copy of the     │
  │     current state                            │
  │ + restoreState(s: EditorSnapshot): void      │
  │   → recovers the state from the Memento      │
  │ + (editing operations)                       │
  └──────────────────────────────────────────────┘
       creates and restores ▲     ▼ stored by
                            │     │
       EditorSnapshot (Memento) — opaque interface toward the Caretaker
  ┌──────────────────────────────────────────────┐
  │ (private fields — only the Editor accesses)  │
  │ - content: string                            │
  │ - cursorPos: number                          │
  │ - selection: [number,number] | null          │
  │                                              │
  │ (no public getters — opaque to the Caretaker)│
  └──────────────────────────────────────────────┘
                            ▲ keeps and hands back
                            │
         History (Caretaker)
  ┌──────────────────────────────────────────────┐
  │ - stack: EditorSnapshot[]                    │
  │                                              │
  │ + save(originator: Editor): void             │
  │   → stack.push(editor.saveState())           │
  │ + undo(originator: Editor): void             │
  │   → editor.restoreState(stack.pop())         │
  │                                              │
  │ !! Never inspects the Memento's content !!   │
  └──────────────────────────────────────────────┘


Comparing approaches to undo:

  Command + incremental undo          Memento + snapshot
  ─────────────────────────────       ──────────────────────────────
  Each Command knows its own undo     Originator knows its own state
  Memory: proportional to the action  Memory: proportional to the state
  Precise for atomic actions          Simple for complex state
  More boilerplate per action type    Less code, more memory

Code examples

Example 1 — Text editor with snapshot-based undo (Memento)

The same domain as Command, but now undo captures the editor's complete state in a snapshot. Note how History (the Caretaker) never accesses any internal data of EditorSnapshot — it just stores it and hands it back. The comparison with Command is made explicit in the comments.

// ── Memento — opaque snapshot of the Editor ───────────────────
// The interface exposed to the Caretaker is opaque: no public getters.
// Only the Editor accesses the fields via a package-restricted method (friend pattern).
class EditorSnapshot {
  // Readonly fields — immutable after the snapshot is created.
  constructor(
    private readonly content: string,
    private readonly cursorPos: number,
    private readonly selection: readonly [number, number] | null
  ) {}

  // Restricted method: in practice only the Editor calls it.
  // In languages with friend/package-private, this would be inaccessible to the Caretaker.
  _recover(): {
    content: string;
    cursorPos: number;
    selection: readonly [number, number] | null;
  } {
    return {
      content: this.content,
      cursorPos: this.cursorPos,
      selection: this.selection ? [...this.selection] : null,
    };
  }
}

// ── Originator — creates and restores Mementos ────────────────
class Editor {
  private content: string = '';
  private cursorPos: number = 0;
  private selection: [number, number] | null = null;

  // Editing operations
  insert(text: string): void {
    this.content =
      this.content.slice(0, this.cursorPos) +
      text +
      this.content.slice(this.cursorPos);
    this.cursorPos += text.length;
  }

  deletePrevious(): void {
    if (this.cursorPos === 0) return;
    this.content =
      this.content.slice(0, this.cursorPos - 1) +
      this.content.slice(this.cursorPos);
    this.cursorPos--;
  }

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

  selectRange(start: number, end: number): void {
    this.selection = [start, end];
  }

  // Creates a snapshot of the COMPLETE state
  saveState(): EditorSnapshot {
    return new EditorSnapshot(
      this.content,
      this.cursorPos,
      this.selection ? [...this.selection] : null
    );
  }

  // Restores from a snapshot
  restoreState(snapshot: EditorSnapshot): void {
    const state = snapshot._recover();
    this.content   = state.content;
    this.cursorPos = state.cursorPos;
    this.selection = state.selection ? [...state.selection] : null;
  }

  toString(): string {
    const sel = this.selection
      ? ` [sel ${this.selection[0]}-${this.selection[1]}]`
      : '';
    return `"${this.content}" cursor=${this.cursorPos}${sel}`;
  }
}

// ── Caretaker — stores the history of snapshots ────────────────
class History {
  private readonly stack: EditorSnapshot[] = [];

  // Saves the editor's current state onto the stack
  save(editor: Editor): void {
    this.stack.push(editor.saveState());
  }

  // Restores the last saved state
  undo(editor: Editor): boolean {
    const snapshot = this.stack.pop();
    if (!snapshot) return false;
    // The Caretaker hands the Memento back to the Originator — never inspects it.
    editor.restoreState(snapshot);
    return true;
  }

  size(): number { return this.stack.length; }
}

// ── Usage ────────────────────────────────────────────────────
const editor  = new Editor();
const history = new History();

// State 1: empty
history.save(editor);
console.log(`[0] ${editor}`);   // "" cursor=0

editor.insert('Hi');
history.save(editor);
console.log(`[1] ${editor}`);   // "Hi" cursor=2

editor.insert(' there');
history.save(editor);
console.log(`[2] ${editor}`);   // "Hi there" cursor=8

editor.selectRange(3, 8);
history.save(editor);
console.log(`[3] ${editor}`);   // "Hi there" cursor=8 [sel 3-8]

// Undo: back to state 3 (before the selection)
history.undo(editor);
console.log(`Undo → ${editor}`); // "Hi there" cursor=8

// Undo: back to state 2 (before " there")
history.undo(editor);
console.log(`Undo → ${editor}`); // "Hi" cursor=2

// Undo: back to state 1 (empty)
history.undo(editor);
console.log(`Undo → ${editor}`); // "" cursor=0

console.log(`Remaining snapshots: ${history.size()}`); // 0

Example 2 — App configuration with named checkpoints

Memento isn't limited to editors. A Caretaker with support for named checkpoints (instead of an anonymous stack) allows saving and restoring states with clear semantics — like "factory settings" vs "user settings" — without exposing the configuration object's internal fields.

// ── Configuration Memento ─────────────────────────────────────
class ConfigSnapshot {
  constructor(
    private readonly data: Readonly<Record<string, unknown>>
  ) {}

  _recover(): Record<string, unknown> {
    return { ...this.data }; // defensive copy
  }
}

// ── Originator ────────────────────────────────────────────────
class AppConfig {
  private data: Record<string, unknown> = {
    theme: 'light',
    language: 'en-US',
    fontSize: 14,
    notifications: true,
  };

  set(key: string, value: unknown): void {
    this.data[key] = value;
  }

  get(key: string): unknown {
    return this.data[key];
  }

  saveState(): ConfigSnapshot {
    return new ConfigSnapshot({ ...this.data });
  }

  restoreState(snapshot: ConfigSnapshot): void {
    this.data = snapshot._recover();
  }

  show(): void {
    console.log(JSON.stringify(this.data, null, 2));
  }
}

// ── Caretaker with named checkpoints ──────────────────────────
class CheckpointManager {
  private readonly checkpoints = new Map<string, ConfigSnapshot>();

  save(name: string, config: AppConfig): void {
    this.checkpoints.set(name, config.saveState());
    console.log(`Checkpoint "${name}" saved.`);
  }

  restore(name: string, config: AppConfig): boolean {
    const snapshot = this.checkpoints.get(name);
    if (!snapshot) {
      console.warn(`Checkpoint "${name}" not found.`);
      return false;
    }
    config.restoreState(snapshot);
    console.log(`Checkpoint "${name}" restored.`);
    return true;
  }

  list(): string[] {
    return [...this.checkpoints.keys()];
  }
}

// ── Usage ────────────────────────────────────────────────────
const config = new AppConfig();
const manager = new CheckpointManager();

// Saves the factory state
manager.save('factory', config);

// User customizes
config.set('theme', 'dark');
config.set('fontSize', 18);
config.set('language', 'pt-BR');
manager.save('user', config);

// More adjustments — without saving
config.set('notifications', false);
config.set('fontSize', 22);
console.log('\nCurrent state:');
config.show();
// { theme: 'dark', language: 'pt-BR', fontSize: 22, notifications: false }

// Restores the user's configuration
manager.restore('user', config);
config.show();
// { theme: 'dark', language: 'pt-BR', fontSize: 18, notifications: true }

// Restores factory settings
manager.restore('factory', config);
config.show();
// { theme: 'light', language: 'en-US', fontSize: 14, notifications: true }

console.log('Available checkpoints:', manager.list());
// ['factory', 'user']

When to use

  • When you need snapshot-based undo/redo: the pattern's central use case. If the Originator's state is compact enough or the undo depth is limited, Memento is simpler to implement than Command with incremental undo for every operation type.
  • When the object's state is rich and interdependent: if multiple fields need to be restored together to guarantee consistency (e.g.: content + cursor + selection in the editor), a complete snapshot is safer than reverting field by field.
  • When you need checkpoints or transactions: saving the state before a potentially destructive operation and restoring it on failure — without exposing the object's internals to whoever manages it.
  • When the Originator's encapsulation must be preserved: the Caretaker needs to keep the state but shouldn't know its internal structure. Memento creates the necessary abstraction barrier.

When to avoid

  • When the Originator's state is large: every snapshot is a copy of the complete state. For objects with megabytes of data, keeping a history of snapshots can exhaust memory. Consider incremental snapshots, compression, or Command with incremental undo.
  • When undo depth is unlimited and the object is large: the combination of large state + many undo levels makes Memento prohibitive in memory. Set a maximum number of snapshots in the Caretaker.
  • When dynamic languages make encapsulation trivial to break: in PHP and JavaScript, the Memento's encapsulation is conventional, not enforced by the compiler. If the Caretaker can access the Memento's fields via reflection, the pattern's conceptual barrier loses its strength.

Pros and cons

Pros

  • Preserves the Originator's encapsulation — the Caretaker doesn't know the internal structure of the state.
  • Simplifies the Originator: the undo logic isn't scattered across every operation — it's centralized in saveState/restoreState.
  • Allows multi-level undo and named checkpoints with the same mechanism.
  • The Caretaker can limit the history's depth by discarding old Mementos — without changing the Originator.

Cons

  • High memory usage for Originators with large state — every snapshot is a complete copy.
  • The Memento's encapsulation is hard to enforce in languages without support for inner classes or packages (like TypeScript and PHP) — it's conventional, not compiler-enforced.
  • If the Originator changes its internal structure, existing Mementos' format can become incompatible (a versioning problem).
  • Requires a defensive copy of the state to prevent the Memento from sharing mutable references with the Originator.

Common pitfalls

1. A Caretaker that inspects the Memento (violates encapsulation)

The pattern's fundamental mistake: the Caretaker calls getters on the Memento to read fields, display information, or make decisions. This creates coupling between the Caretaker and the Originator's internal structure — exactly what the pattern aims to avoid. The Caretaker should treat the Memento as a black box: receive it, store it, hand it back. Nothing more.

Warning sign: if the Caretaker has an if (snapshot.getContent().length > 0) somewhere, encapsulation has been violated.

2. Shared mutable references (a shallow snapshot)

The most critical technical pitfall: saveState() copies references instead of values. If the Originator's state contains mutable objects or arrays and the Memento keeps the reference (not a deep copy), the snapshot will reflect later changes — making it useless for restoration. Golden rule: the Memento must be immutable and contain only copies of the data, never the original references.

// WRONG: shared reference — the snapshot changes along with the Originator
class EditorSnapshot {
  constructor(readonly data: string[]) {} // reference!
}
const snap = new EditorSnapshot(editor.lines); // editor.lines and snap.data point to the same array
editor.lines.push('new line'); // corrupted the snapshot!

// CORRECT: defensive copy
class EditorSnapshot {
  constructor(readonly data: readonly string[]) {}
}
const snap = new EditorSnapshot([...editor.lines]); // copy — independent

3. Incompatible Mementos after refactoring (versioning)

If the Originator is serialized for persistence (game save files, user sessions) and the Memento's format changes with a refactoring, old snapshots may become unrestorable. Consider including a version number in the Memento and implementing migration when the format changes — especially if Mementos are persisted beyond the current session.

4. An unbounded history

A Caretaker that accepts unlimited Mementos can gradually exhaust memory. Set a maximum limit (e.g.: 50 snapshots) on the Caretaker and discard the oldest ones once the limit is reached. This is a decision made by the Caretaker — the Originator and the Memento don't need to know about it.

Related patterns

Memento interacts with behavioral patterns that also deal with action history and state management:

Command is Memento's natural and most frequent complement. The critical distinction: Command undoes a specific action incrementally — each Command knows exactly how to revert what it did. Memento restores a complete snapshot of the state — it doesn't undo an action, it returns the object to an earlier point in time. The two are complementary: a Command can use originator.saveState() in its execute() and originator.restoreState(snapshot) in its undo(), delegating the responsibility of serializing itself to the Originator — without the Command needing to know the Receiver's internal fields. State also manages an object's state, but its focus is representing discrete states with explicit transitions, not capturing and restoring arbitrary states. Memento can complement State when it's necessary to return to a prior state after a transition — the Memento stores the snapshot of the StateContext before the transition.