Skip to content
aipatterns.dev

Design patternBehavioral Patterns

Memento

Capture an object's internal state in an opaque snapshot so it can be restored later without exposing that state to the code holding the snapshot.

Complexity
High
Seen in the wild
Low
Also known as
Snapshot, Token

Intent

Let an object save its own state into a snapshot object and later restore from it. Other code can store and pass the snapshot around, but can't read or change what is inside. Rollback and undo work without breaking the object's encapsulation.

Forces

  • Rolling back vs keeping fields private: the code that decides to roll back should not need to know which fields to reset.
  • Trying a change vs predicting its effect: sometimes the only way to know whether a change is valid is to apply it and check.
  • Complete snapshots vs memory cost: copying the whole state is simple and correct, but expensive when state is large or snapshots are frequent.
  • Shallow copies vs shared mutable parts: a snapshot that shares a mutable list with the live object is not a snapshot.

Therefore

Have the object (the originator) produce and consume its own snapshots, and give everyone else an opaque handle to hold.

The originator's save() copies its internal state into a memento, and restore(m) copies it back. A separate caretaker decides when to save and restore and keeps the mementos, but treats them as tokens. Each language enforces "opaque" differently: nested classes with private fields in Java, unexported fields in Go, a private lookup table or brand type in TypeScript, and naming convention in Python.

Structure

Diagram source (Mermaid)
classDiagram
  class RoutePlan {
    -Stop[] stops
    -int shiftMin
    +add(stop) void
    +minutes() int
    +save() RouteSnapshot
    +restore(s) void
  }
  class RouteSnapshot {
    -Stop[] stops
  }
  class Dispatcher {
    +planDay(route, batches) void
  }
  class Stop {
    -String id
    -int driveMin
    -int serviceMin
  }
  RoutePlan *-- Stop
  RoutePlan ..> RouteSnapshot : creates
  Dispatcher ..> RouteSnapshot : holds
  Dispatcher --> RoutePlan

Participants

  • Originator (RoutePlan): the object whose state is saved. Only it can create a memento or read one.
  • Memento (RouteSnapshot): an immutable copy of the originator's state, opaque to everyone else.
  • Caretaker (Dispatcher): decides when to snapshot and when to roll back, and stores mementos without looking inside them.

Use it when

  • You apply a change tentatively, check the result, and roll back on failure, and the change touches state the caller can't see.
  • You need undo for an object whose internals are private, and writing an inverse for every operation is impractical.
  • You need checkpoints in a long-running computation (a solver, an import, a simulation) to resume or backtrack.
  • Several fields must be restored together, and resetting them one by one from outside would leak the object's structure.

Do not use it when

  • The state is already immutable. Keep a reference to the previous value; persistent data structures and reducer-style state (state = reduce(state, action)) give you snapshots for free.
  • You can validate before mutating. Check the change first and skip the snapshot. It is cheaper and simpler.
  • The state is a plain public record with no invariants to protect. Copy it (structuredClone, copy.deepcopy, a copy constructor); encapsulation was the only reason for the pattern.
  • State is large and changes are small and frequent. Store deltas with Command and undo instead of full copies.
  • The snapshot must survive a restart or cross a process. That is serialization or an event log with a versioned format, not an in-memory memento.

Application grill

Inspect first: the state being snapshotted and its size; existing undo or history features (editors, forms) and their storage; whether snapshots hold personal data that crosses a trust boundary or retention rule.

Missing pieces: in-memory: the language's value copies - nothing to install. Persisted: a table or blob store with a retention and deletion policy; snapshots of personal data must age out on the same schedule as the records they mirror.

Ask before you build:

  • Scale: what is snapshot size times retention count - memory or storage bound?
  • Performance: snapshot on every change or per action, and is coalescing needed?
  • Reliability: under concurrent edits, what does restore mean - is the snapshot stale the moment it restores?
  • Budget: who pays the retention storage and runs the deletion pipeline?
  • Implementation: when the internal state shape changes, do old snapshots still restore (versioned snapshots), and opaque to whom?

Implementation steps

  1. List every field that has to come back on restore, including derived fields and caches, or recompute them after restore.
  2. Create the memento type with those fields, immutable, and make it unreadable outside the originator.
  3. Add save() to the originator. Copy mutable collections deeply enough that later changes don't leak into the snapshot.
  4. Add restore(memento). Copy again on the way in, so restoring twice from the same memento works.
  5. Move the "when to save and roll back" logic into a caretaker that holds mementos as opaque values.
  6. Test that save, then a change, then restore gives back an object equal to the original, and that the same memento can be restored twice.

Example

A dispatch tool builds a delivery van's route for the day. Stops arrive in batches that must be kept together (several drops for one customer). The dispatcher adds each batch, and if the route no longer fits the shift, rolls the route back to its state before that batch.

interface Stop {
  readonly id: string;
  readonly driveMin: number;
  readonly serviceMin: number;
}

// Opaque to callers: they can hold a snapshot but cannot read or build one.
declare const brand: unique symbol;
type RouteSnapshot = { readonly [brand]: true };

class RoutePlan {
  private static readonly saved = new WeakMap<RouteSnapshot, readonly Stop[]>();
  private stops: Stop[] = [];

  constructor(readonly shiftMin: number) {}

  add(stop: Stop): void {
    this.stops.push(stop);
  }
  minutes(): number {
    return this.stops.reduce((t, s) => t + s.driveMin + s.serviceMin, 0);
  }
  ids(): string[] {
    return this.stops.map((s) => s.id);
  }

  save(): RouteSnapshot {
    const snapshot = Object.freeze({}) as RouteSnapshot;
    RoutePlan.saved.set(snapshot, [...this.stops]); // stops are immutable, so a shallow copy is enough
    return snapshot;
  }

  restore(snapshot: RouteSnapshot): void {
    const stops = RoutePlan.saved.get(snapshot);
    if (!stops) throw new Error("snapshot was not created by RoutePlan");
    this.stops = [...stops];
  }
}

// Caretaker: decides when to roll back, never looks inside the snapshot.
function planDay(route: RoutePlan, batches: Stop[][]): void {
  for (const batch of batches) {
    const checkpoint = route.save();
    batch.forEach((s) => route.add(s));
    if (route.minutes() > route.shiftMin) {
      route.restore(checkpoint);
      console.log("rejected:", batch.map((s) => s.id).join(","));
    }
  }
}

const route = new RoutePlan(480);
planDay(route, [
  [{ id: "s1", driveMin: 60, serviceMin: 15 }, { id: "s2", driveMin: 40, serviceMin: 20 }],
  [{ id: "s3", driveMin: 90, serviceMin: 30 }, { id: "s4", driveMin: 50, serviceMin: 10 }],
  [{ id: "s5", driveMin: 120, serviceMin: 30 }, { id: "s6", driveMin: 45, serviceMin: 15 }],
  [{ id: "s7", driveMin: 30, serviceMin: 20 }],
]);
console.log(route.ids(), route.minutes()); // [ 's1', 's2', 's3', 's4', 's7' ] 365

In Java, nested classes share private access within their top-level class, so Dispatch could technically read Snapshot.stops. In real code, put RoutePlan in its own file.

Trade-offs

Gains

  • Rollback and undo without exposing the originator's fields.
  • The caretaker's logic stays simple: save, try, restore.
  • One restore resets every related field together, so invariants hold.
  • Snapshots are immutable and safe to keep in a history.

Costs

  • Each snapshot copies state. Large state or frequent snapshots cost memory and time.
  • Deep versus shallow copying is easy to get wrong, and the bug only shows on a later mutation.
  • Opaqueness is weak in some languages (Python convention, Go package scope).
  • Caretakers that keep every snapshot need a retention limit.

Relations

  • Command records the operation and its inverse. Memento records the state. Use a memento inside a command when the operation has no cheap inverse.
  • Iterator positions can be saved as mementos so a long traversal can resume.
  • Prototype clones the whole object. A memento copies only the state needed to restore it, and keeps it opaque.
  • State objects can be stored in a memento when the current state is part of what must roll back.

Agent checklist

  • You checked that validating before mutating, or keeping an immutable previous value, doesn't already solve the problem.
  • Every field that must come back is in the snapshot, or is recomputed after restore.
  • Mutable collections are copied on save and on restore, and a test mutates after saving to prove the snapshot is unaffected.
  • Caretakers hold mementos as opaque values, and the history has a size or age limit.
  • A round-trip test (save, change, restore) asserts the originator equals its original state.

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.