InterviewKata
Design patterns · 22 of 26 · Objects talking

Memento

Memento lets an object hand out snapshots of its own state and restore itself from them later, while nobody else can read them. This page adds undo to Pedal's route planner with immutable snapshots, and shows the bug a snapshot that isn't a copy causes.

1. The pain

Pedal's app lets riders plan a route: pick a start and end station, add stops along the way, choose "avoid hills". Riders kept asking for undo. The first version did it in the screen: before each change, the screen copied plan.stops and plan.avoid_hills into its own history list, and on undo it wrote them back.

So the screen had to know every field of the plan. When the plan gained "e-bikes only", undo silently ignored it. And one copy was taken without duplicating the list, so it changed along with the plan and undo restored nothing.

2. The idea in one picture

A save point in a video game. You don't need to know how the game stores your health, inventory and position. You press save, you get a slot; later you load the slot and everything is back as it was. Only the game can read the slot.

Memento lets an object (the originator) produce a snapshot of its own state, and later restore itself from it, without exposing that state to anyone else. RoutePlan.save() returns an opaque, immutable Snapshot; History (the caretaker) stores snapshots and hands them back for undo, but never looks inside. When the plan gains a field, only save() and restore() change.

Memento and command both give undo. Command reverses each action (needs an inverse for every operation, stores little); memento restores a whole snapshot (needs no inverse, may store a lot). They're often combined: a command keeps a memento of the state it changed.

3. The roles

RoleIts jobIn Pedal
OriginatorCreates snapshots of itself and restores from them.RoutePlan with save() and restore()
MementoThe snapshot: immutable and opaque to everyone but the originator.RoutePlan.Snapshot
CaretakerKeeps snapshots and decides when to restore; never reads them.History with its undo stack

4. Before and after

Undo both ways (Python sketch; the full program is in section 5).

Without the pattern

# in the screen
def on_add_stop(station):
    history.append((plan.stops,           # not copied!
                    plan.avoid_hills))
    plan.add_stop(station)

def on_undo():
    plan.stops, plan.avoid_hills = history.pop()
    # e-bikes-only? forgotten

With it

class RoutePlan:
    def save(self):
        return Snapshot(tuple(self.stops),
                        self.avoid_hills)

    def restore(self, snap):
        self.stops = list(snap._stops)
        self.avoid_hills = snap._avoid_hills

# in the screen: never looks inside
history.edit("add S-31", lambda: plan.add_stop("S-31"))
history.undo()
The next changeWithout the patternWith it
The plan gains "e-bikes only"Every screen's undo must learn the fieldsave() and restore() include it
Make stops privateBreaks the screens that copy itNothing outside the plan reads it
Save the plan when the app closesAnother copy of the field listStore the last snapshot

The object that owns the state is the only one that knows how to capture and restore it.

5. The code

RoutePlan saves its stops and settings into an immutable snapshot before each edit; History keeps the snapshots and hands them back on undo without reading them.

from __future__ import annotations
from typing import Callable


class RoutePlan:
    """The originator: a route the rider is editing. Only it can read or apply its own snapshots."""

    class Snapshot:
        """The memento: opaque to everyone else, and immutable (a tuple, not the live list)."""
        def __init__(self, stops: tuple[str, ...], avoid_hills: bool) -> None:
            self._stops, self._avoid_hills = stops, avoid_hills

    def __init__(self, start: str, end: str) -> None:
        self.stops = [start, end]
        self.avoid_hills = False

    def add_stop(self, station: str) -> None:
        self.stops.insert(len(self.stops) - 1, station)

    def set_avoid_hills(self, on: bool) -> None:
        self.avoid_hills = on

    def save(self) -> RoutePlan.Snapshot:
        return RoutePlan.Snapshot(tuple(self.stops), self.avoid_hills)

    def restore(self, snap: RoutePlan.Snapshot) -> None:
        self.stops, self.avoid_hills = list(snap._stops), snap._avoid_hills

    def describe(self) -> str:
        return f"{' -> '.join(self.stops)} | avoid hills: {'yes' if self.avoid_hills else 'no'}"


class History:
    """The caretaker: keeps snapshots for undo without looking inside them."""

    def __init__(self, plan: RoutePlan) -> None:
        self.plan = plan
        self.undo_stack: list[RoutePlan.Snapshot] = []

    def edit(self, change: str, apply: Callable[[], None]) -> str:
        self.undo_stack.append(self.plan.save())   # snapshot before every change
        apply()
        return f"{change}: {self.plan.describe()}"

    def undo(self) -> str:
        self.plan.restore(self.undo_stack.pop())
        return f"undo: {self.plan.describe()}"


if __name__ == "__main__":
    plan = RoutePlan("S-12", "S-40")
    history = History(plan)
    print(f"start: {plan.describe()}")
    print(history.edit("add S-31", lambda: plan.add_stop("S-31")))
    print(history.edit("avoid hills", lambda: plan.set_avoid_hills(True)))
    print(history.edit("add S-33", lambda: plan.add_stop("S-33")))
    print(history.undo())
    print(history.undo())
    print(f"snapshots left: {len(history.undo_stack)}")
// The memento: immutable, and only RoutePlan reads it.
class Snapshot {
  constructor(readonly stops: readonly string[], readonly avoidHills: boolean) {}
}

// The originator: a route the rider is editing.
class RoutePlan {
  private stops: string[];
  private avoidHills = false;
  constructor(start: string, end: string) { this.stops = [start, end]; }

  addStop(station: string): void { this.stops.splice(this.stops.length - 1, 0, station); }
  setAvoidHills(on: boolean): void { this.avoidHills = on; }
  save(): Snapshot { return new Snapshot([...this.stops], this.avoidHills); }
  restore(s: Snapshot): void { this.stops = [...s.stops]; this.avoidHills = s.avoidHills; }
  describe(): string { return `${this.stops.join(" -> ")} | avoid hills: ${this.avoidHills ? "yes" : "no"}`; }
}

// The caretaker: keeps snapshots for undo without looking inside them.
class History {
  readonly undoStack: Snapshot[] = [];
  constructor(private readonly plan: RoutePlan) {}
  edit(change: string, apply: () => void): string {
    this.undoStack.push(this.plan.save());   // snapshot before every change
    apply();
    return `${change}: ${this.plan.describe()}`;
  }
  undo(): string { this.plan.restore(this.undoStack.pop()!); return `undo: ${this.plan.describe()}`; }
}

const plan = new RoutePlan("S-12", "S-40");
const history = new History(plan);
console.log(`start: ${plan.describe()}`);
console.log(history.edit("add S-31", () => plan.addStop("S-31")));
console.log(history.edit("avoid hills", () => plan.setAvoidHills(true)));
console.log(history.edit("add S-33", () => plan.addStop("S-33")));
console.log(history.undo());
console.log(history.undo());
console.log(`snapshots left: ${history.undoStack.length}`);
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class Main {
    // The originator: a route the rider is editing.
    static class RoutePlan {
        // The memento: immutable; its fields are private to RoutePlan's nesting, so other classes can't read them.
        static final class Snapshot {
            private final List<String> stops;
            private final boolean avoidHills;
            private Snapshot(List<String> stops, boolean avoidHills) { this.stops = List.copyOf(stops); this.avoidHills = avoidHills; }
        }

        private List<String> stops = new ArrayList<>();
        private boolean avoidHills;
        RoutePlan(String start, String end) { stops.add(start); stops.add(end); }

        void addStop(String station) { stops.add(stops.size() - 1, station); }
        void setAvoidHills(boolean on) { avoidHills = on; }
        Snapshot save() { return new Snapshot(stops, avoidHills); }
        void restore(Snapshot s) { stops = new ArrayList<>(s.stops); avoidHills = s.avoidHills; }
        String describe() { return String.join(" -> ", stops) + " | avoid hills: " + (avoidHills ? "yes" : "no"); }
    }

    // The caretaker: keeps snapshots for undo without looking inside them.
    static class History {
        private final RoutePlan plan;
        final Deque<RoutePlan.Snapshot> undoStack = new ArrayDeque<>();
        History(RoutePlan plan) { this.plan = plan; }
        String edit(String change, Runnable apply) {
            undoStack.push(plan.save());   // snapshot before every change
            apply.run();
            return change + ": " + plan.describe();
        }
        String undo() { plan.restore(undoStack.pop()); return "undo: " + plan.describe(); }
    }

    public static void main(String[] args) {
        RoutePlan plan = new RoutePlan("S-12", "S-40");
        History history = new History(plan);
        System.out.println("start: " + plan.describe());
        System.out.println(history.edit("add S-31", () -> plan.addStop("S-31")));
        System.out.println(history.edit("avoid hills", () -> plan.setAvoidHills(true)));
        System.out.println(history.edit("add S-33", () -> plan.addStop("S-33")));
        System.out.println(history.undo());
        System.out.println(history.undo());
        System.out.println("snapshots left: " + history.undoStack.size());
    }
}

Output (the same in all three languages)

start: S-12 -> S-40 | avoid hills: no
add S-31: S-12 -> S-31 -> S-40 | avoid hills: no
avoid hills: S-12 -> S-31 -> S-40 | avoid hills: yes
add S-33: S-12 -> S-31 -> S-33 -> S-40 | avoid hills: yes
undo: S-12 -> S-31 -> S-40 | avoid hills: yes
undo: S-12 -> S-31 -> S-40 | avoid hills: no
snapshots left: 1

6. Try it

Edit a route, then undo

Each change saves a snapshot first. Try the broken version, where a snapshot keeps a reference to the live list of stops instead of a copy.

Snapshots in History (newest last)

    7. When not to use it

    What you get

    • Encapsulation kept: nobody else reads the plan's fields.
    • Simple undo: no inverse operation needed for each change.
    • One place to update: new fields change save() and restore() only.
    • Snapshots are values: safe to store, compare, persist.

    What you pay

    • Memory: a full snapshot per change adds up for big objects.
    • Copy cost: snapshots of large state take time to make.
    • Deep-copy traps: a snapshot holding live references isn't a snapshot.
    • Language limits: TypeScript can't hide a snapshot's fields from other classes; convention has to do.
    If…Use instead
    State is large and changes are small16 Command with undo, or store diffs
    You need the object's history over time, not just undoEvent sourcing: store the changes
    The state is already immutableKeep references to old versions: they are the snapshots
    You need a copy to start a new object from04 Prototype

    8. How it goes wrong

    Snapshots that share mutable dataSaving self.stops instead of a copy means the snapshot changes with the plan. Copy into an immutable form.
    Caretakers that read snapshotsIf History inspects stops to show a preview, it depends on the plan's internals again. Ask the plan for a description instead.
    Unbounded historyA snapshot per keystroke for a long session uses memory without limit. Cap the stack or merge small edits.
    Restoring into the wrong objectA snapshot from one plan applied to another may mix ids. Tag snapshots with their owner if that can happen.

    9. Remember it and say it

    Memory hook: "A save point"Press save, get a slot you can't read; load it later and everything's back.
    How this hook works: what “A save point” means

    What the slogan means, phrase by phrase

    “Save point”
    save() returns a snapshot.
    “a slot you can't read”
    An opaque memento.
    “load it later”
    restore(snapshot).

    Why this picture fits: Games let players rewind without showing them the game's internal state.

    The story → the concept

    The gameThe code
    The gameRoutePlan (originator)
    A save slotSnapshot (memento)
    The save menuHistory (caretaker)
    Loading a slotrestore()

    Watch it on an example

    1. Before adding S-33, History asks the plan to save: a snapshot of stops and settings.
    2. S-33 is added.
    3. Undo hands the snapshot back; the plan restores itself. History never looked inside.

    The trap it saves you from

    Screens copying an object's fields for undo, and missing the ones added later.

    It still works when…

    Yes: editor undo, "discard changes" in forms, game saves and database savepoints.

    In an interview or code review

    10. Check yourself

    Who creates and reads a memento?

    The caretaker stores it unopened.

    The plan gains an "e-bikes only" field. What changes?

    Nothing outside the plan knows its fields.

    A snapshot stores self.stops without copying. What happens on undo?

    Snapshots must not share mutable data.

    Memento vs command for undo?

    They're often combined.

    What's a memento's main cost?

    One snapshot per change adds up.

    11. Go deeper

    ResourceWhy
    16 CommandUndo by reversing actions.
    04 PrototypeCopies made for a different reason: starting new objects.
    PostgreSQL SAVEPOINTMementos inside a database transaction.
    Wikipedia: Memento patternRoles and examples.