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
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
| Role | Its job | In Pedal |
|---|---|---|
| Originator | Creates snapshots of itself and restores from them. | RoutePlan with save() and restore() |
| Memento | The snapshot: immutable and opaque to everyone but the originator. | RoutePlan.Snapshot |
| Caretaker | Keeps 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? forgottenWith 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 change | Without the pattern | With it |
|---|---|---|
| The plan gains "e-bikes only" | Every screen's undo must learn the field | save() and restore() include it |
| Make stops private | Breaks the screens that copy it | Nothing outside the plan reads it |
| Save the plan when the app closes | Another copy of the field list | Store 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.
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()andrestore()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 small | 16 Command with undo, or store diffs |
| You need the object's history over time, not just undo | Event sourcing: store the changes |
| The state is already immutable | Keep references to old versions: they are the snapshots |
| You need a copy to start a new object from | 04 Prototype |
8. How it goes wrong
self.stops instead of a copy means the snapshot changes with the plan. Copy into an immutable form.History inspects stops to show a preview, it depends on the plan's internals again. Ask the plan for a description instead.9. Remember it and say it
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 game | The code |
|---|---|
| The game | RoutePlan (originator) |
| A save slot | Snapshot (memento) |
| The save menu | History (caretaker) |
| Loading a slot | restore() |
Watch it on an example
- Before adding S-33, History asks the plan to save: a snapshot of stops and settings.
- S-33 is added.
- 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
- Name the three roles: the plan saves and restores itself; snapshots are opaque; History only stores them.
- Mention immutability: snapshots copy the stops into a tuple, so they can't change with the plan.
- Compare with command undo: memento stores state, command stores the inverse action; pick by state size and change size.
- Cap it: limit the history length or snapshot on meaningful edits only.
10. Check yourself
Who creates and reads a memento?
The plan gains an "e-bikes only" field. What changes?
A snapshot stores self.stops without copying. What happens on undo?
Memento vs command for undo?
What's a memento's main cost?
11. Go deeper
| Resource | Why |
|---|---|
| 16 Command | Undo by reversing actions. |
| 04 Prototype | Copies made for a different reason: starting new objects. |
PostgreSQL SAVEPOINT | Mementos inside a database transaction. |
| Wikipedia: Memento pattern | Roles and examples. |