Memento pattern
Open in the interactive app: practice, quiz, mock interview →In one minute
- A memento is a snapshot of an object's internal state, created by the object itself, stored by someone else, and handed back later to restore that state. The holder can keep, label and discard snapshots but cannot read or change what is inside them.
- There are three roles: the originator (the object whose state is saved), the memento (the opaque snapshot) and the caretaker (the history, checkpoint store or save-game slot list that decides which snapshots to keep).
- It is the alternative to the inverse-operation undo in the Command lesson. Commands store what changed; mementos store what was. Mementos are simpler to get right and cost memory proportional to state, unless snapshots share the parts that did not change.
- Structural sharing is what makes mementos affordable: with immutable pieces, a snapshot is a table of pointers, and two snapshots that differ in one region share every other region. Redux undo, persistent vectors and copy-on-write
fork()all work this way. - Agents write
history.append(self.state)and call it done. Review asks: is that a copy or an alias, how big is one snapshot, and what bounds the history?
How it works
The problem
A level editor for a 2D game holds a world split into 256 regions. Designers place tiles, scripts and spawn points, and they want three things: undo for the last few minutes, named checkpoints ("before boss rework"), and a "playtest from here" button that runs the game and then puts the world back exactly as it was. Some operations are easy to invert (move a tile), but many are not: a procedural "fill with forest" brush touches thousands of tiles with random choices, and a script compiler rewrites internal indexes. Writing a correct inverse for each is a project in itself.
The shortcut is to let the history copy the world's fields itself. Then the history knows the world's private layout, and any code holding it can edit a saved state.
The three roles
Memento keeps the knowledge of the layout inside the object that owns it:
class World:
def snapshot(self, label):
return Snapshot(self, tuple(self._regions), label) # originator writes it
def restore(self, snap):
if snap._owner is not self:
raise ValueError("foreign snapshot")
self._regions = list(snap._regions) # originator reads it
class CheckpointStore: # caretaker: keeps, labels, evicts, never looks inside
def save(self, snap): ...The caretaker sees a narrow interface: a label, a version, perhaps a size for a memory budget. The originator sees everything. C++ does this with friend, Java with a nested class, Go with unexported fields in one package, TypeScript with #private fields, and Python by the leading-underscore convention.
A snapshot must not alias live state
The most common memento bug is storing a reference instead of a value. If snapshot() returns the live list, the next edit changes the "saved" state and undo silently does nothing. The mirror bug is restore() installing the snapshot's list directly, so the next edit mutates the memento and restoring the same checkpoint twice gives two different worlds. Both snapshot and restore copy the container.
Structural sharing
Copying the whole world on every snapshot is correct and expensive. If the world is 256 regions of 64 KB each (16 MB) and the editor snapshots every 2 seconds for a 10-minute undo window, that is 300 snapshots and 4.8 GB. But a designer working for 2 seconds touches one or two regions. If regions are immutable values and an edit replaces a region with a new one, a snapshot only has to copy the table of 256 pointers (2 KB on a 64-bit machine). Every unchanged region is shared by every snapshot that saw it. The 300 snapshots then cost 600 KB of tables plus the regions actually edited, typically tens of megabytes instead of gigabytes.
Redux's undo history works the same way: past, present and future arrays of states, where reducers return new objects only along the changed path. Clojure's persistent vectors are 32-way trees, so changing one element of a million copies about four nodes and shares the rest.
Bounding the caretaker
A history without a bound is a memory leak with an undo button. Real caretakers bound by count (the last 100 states), by time (the last 10 minutes), by bytes (500 MB), or by some mix, and they protect a few snapshots from eviction: named checkpoints, the last autosave, the state a playtest must return to. With structural sharing, "bytes" means distinct bytes: evicting the oldest snapshot frees only the regions no remaining snapshot references, so the caretaker needs per-snapshot size metadata (piece IDs and sizes), not the content. That metadata is the right amount of leakage from the memento.
Memento versus Command
Command stores the edit and its inverse; Memento stores the state before. Commands are small, but every operation needs a correct inverse. Mementos need no inverse logic, but cost memory and capture everything, including things you did not want rolled back, such as a cursor position. Serious editors combine them: commands for fine-grained undo, periodic mementos as checkpoints so recovery replays only a short tail. Databases do the same with a write-ahead log plus checkpoints.
Reviewing agent-written code
Asked to "add undo", an agent usually writes self.history.append(copy.deepcopy(self.state)) on every change, or appends without a copy. Ask three questions in review. Does a test edit the object after a snapshot and then restore? How big is one snapshot of the largest realistic state, and what bounds the history? Can code outside the originator modify a snapshot? The fix is usually small: immutable pieces, a pointer-table snapshot, and a bounded caretaker.
The same shape elsewhere
Memento is the multi-step consistency shape: save a known-good point so a failed sequence of steps can be rolled back as a unit. Database savepoints (ROLLBACK TO s1), VM snapshots before an upgrade and kubectl rollout undo all do it. ZFS and Btrfs snapshots are copy-on-write trees, created in constant time and costing nothing until blocks diverge: structural sharing on disk.
Down the stack
Layer 1The contract
The originator promises: restore(snapshot(x)) returns the object to a state observably equal to the state when the snapshot was taken, no matter what happened in between, and restoring the same snapshot twice gives the same result. A memento is immutable and opaque: no code outside the originator can change what it will restore. Restoring a snapshot from a different originator, or from an incompatible version, fails without changing the object. The caretaker promises a bound (count, bytes, age) and an eviction order, and documents which snapshots are pinned.
Layer 2The mechanism
A pointer-table memento needs two things from the state: it is split into pieces, and pieces are never mutated in place. An edit allocates a new piece and swaps the pointer in the live table; the snapshot copies the table. Persistent data structures generalise this to trees. A hash array mapped trie (HAMT), used by Clojure, Scala and Immutable.js maps, branches 32 ways using 5 bits of the key's hash per level, with a 32-bit bitmap per node recording which children exist. Updating one key copies the nodes on one root-to-leaf path, so a snapshot of a million-entry map is one root pointer, and an edit afterwards costs about four node copies. Immer gets the same effect with JavaScript Proxy objects: the draft records which nested objects were written, and only those and their ancestors are copied when the producer finishes.
The operating system offers a memento for a whole process. Redis's BGSAVE calls fork(): the child gets the parent's address space as copy-on-write pages, serialises a frozen view of the dataset to an RDB file, and the parent keeps serving writes. Only pages written after the fork are physically copied.
Layer 3The machine
A full copy is bounded by memory bandwidth: memcpy runs at roughly 10 to 20 GB/s per core, so copying 16 MB takes about 1 ms, fine once and painful every keystroke. Object-graph copies are far slower than raw bytes. CPython's copy.deepcopy visits every object, keeps a memo dict keyed by id() to preserve shared references and cycles, and costs on the order of 1 to 3 microseconds per object, so a state with 200,000 small objects takes about half a second. JavaScript's structuredClone is similar in spirit and also walks the graph. A pointer-table snapshot of 256 regions is a 2 KB allocation and a copy that fits in L1 cache: well under a microsecond.
Go has a trap here. Copying a struct copies its fields, but slices and maps are headers that point at shared backing storage. snap := *w shares the regions slice's backing array with the live world, so a later w.regions[0] = r changes the snapshot. A correct Go memento uses slices.Clone or copy for slices and maps.Clone for maps, and the pieces themselves must be immutable or cloned.
The fork() memento is paid in page tables and copy-on-write faults. With 4 KB pages, each GB of resident memory needs 2 MB of page-table entries, copied at fork time. After that, each page the parent writes takes a fault and a 4 KB copy, so a write-heavy Redis can nearly double its memory during a save.
Why it's built this way
- Saving state should not require the saver to know the layout, or the layout leaks into every caller, so the owner of the state creates and reads snapshots.
- A snapshot that can be modified, or that aliases live state, makes restore wrong, so mementos are immutable and opaque.
- Full copies cost memory and bandwidth proportional to the whole state while edits touch a small part, so state is split into immutable pieces that snapshots share.
- Even shared snapshots accumulate, so the caretaker enforces a bound, measured in distinct pieces or bytes, and pins the snapshots that matter.
- Restoring a whole state also restores things you wanted to keep, so fine-grained undo often uses commands with mementos as periodic checkpoints.
Diagram
sequenceDiagram
participant E as Editor UI
participant W as World (originator)
participant C as CheckpointStore (caretaker)
E->>W: set(region 7, forest)
W->>W: new piece #812, swap pointer
E->>W: snapshot("before boss")
W-->>E: Snapshot(table of 256 refs)
E->>C: save(snapshot)
C->>C: count distinct piece ids
C->>C: evict oldest while over budget
E->>C: find("before boss")
C-->>E: Snapshot
E->>W: restore(snapshot)
W->>W: check owner, copy tableWorked example
A web-based level editor kept undo as a list of full JSON copies of the world, taken after every brush stroke. A medium level serialised to 9 MB, designers made about 25 strokes a minute, and the history held 200 entries: 1.8 GB in the worst case. Browser tabs crashed on 8 GB laptops after about an hour, and each stroke spent 70 ms in JSON.parse(JSON.stringify(world)), which designers felt as a stutter.
The team split the world into 512 regions of about 18 KB each, made regions immutable (an edit produces a new region object), and made a snapshot a frozen array of 512 region references with a monotonic ID per region. The history became a caretaker with two bounds: 200 snapshots and 300 MB of distinct regions, computed from the set of region IDs across retained snapshots with each region's size stored beside it. Named checkpoints and the pre-playtest snapshot were pinned.
A typical stroke touches 1 to 3 regions, so 200 snapshots retained about 450 regions beyond the base 512: under 20 MB in total against 1.8 GB before, and 4 microseconds per snapshot instead of 70 ms. The one regression was "regenerate terrain", which replaces every region and evicted most of the history; the fix was a warning and a pinned snapshot before the operation.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Full-copy memento | Simple; always correct if the copy is deep | Memory and time proportional to whole state | Small state, infrequent snapshots |
| Pointer-table memento with immutable pieces | Snapshot is O(pieces); unchanged pieces shared | State must be split and pieces kept immutable | Editors, games, large documents |
| Persistent data structures (HAMT, persistent vector) | O(log n) edits; snapshot is one pointer | Slower reads and writes than mutable structures; allocation churn | Functional state stores, Redux-style apps |
| Inverse commands | Memory proportional to edits | Every operation needs a correct inverse | Fine-grained undo with well-defined operations |
| Log plus periodic checkpoints | Bounded recovery time and bounded memory | Two mechanisms to build and test | Databases, collaborative editors, long sessions |
Pitfalls in production
- Snapshots that alias live state, so undo restores the current state. Fix: copy the container in both
snapshotandrestore, and test edit-after-snapshot. - Shallow copies of nested mutable pieces: the table is copied but a region is edited in place. Fix: make pieces immutable (frozen dataclasses,
Object.freezein development, no setters), or deep-copy on write. - Unbounded history that turns a long session into an out-of-memory crash. Fix: bound by count and distinct bytes, and measure the largest realistic state.
- Mementos that capture things that should not roll back, such as an auto-incrementing ID counter, a network connection or a selection. Fix: decide explicitly which fields belong to the snapshot and keep the rest outside it.
fork()-based snapshots doubling memory on write-heavy hosts. Fix: leave headroom, disable transparent huge pages, and schedule saves off-peak.
Leadership lens
- Ask for the memory budget first. "How big is one snapshot of the largest real document, and how many do we keep?" turns undo from a feature into an engineering decision.
- Choose deliberately between commands and mementos, or both. Inverse commands are cheaper but every new operation becomes a correctness risk; snapshots are safe but cost memory. That is a staffing and testing question as much as a design one.
- Treat persisted mementos (save games, checkpoints) as a public format: version them and test restoring old ones in CI.
- Give agents a concrete spec: immutable pieces, snapshot is a table of references, caretaker with count and byte limits, tests for aliasing and eviction. Without it you will get a deep copy per keystroke.
Check yourself
Why must the originator, not the caretaker, create and read the snapshot?
Show answer
Only the originator knows its internal layout and invariants. If the caretaker copied fields directly, the layout would leak into it, and any change to the originator would need matching changes in every caretaker.
A snapshot copies a list of 256 region references. What property of the regions makes that a correct snapshot rather than an alias?
Show answer
Regions must be immutable, so an edit replaces a reference in the live list instead of changing a region shared with the snapshot.
With structural sharing, why does evicting the oldest snapshot sometimes free nothing?
Show answer
Every piece it references may still be referenced by newer snapshots, so memory is freed only for pieces that no retained snapshot uses.
When would you combine mementos with commands instead of picking one?
Show answer
When you want small, fine-grained undo but also bounded recovery or replay time: commands for each edit, periodic mementos as checkpoints so a restore replays only the commands since the last one.
Watch
- Memento Design Pattern (Derek Banas): the originator, memento and caretaker roles built step by step in Java.
- Memento Design Pattern: Easy Guide for Beginners (Hello Byte): a short walkthrough of the pattern's structure and an undo example.
Free resources
- Memento (Refactoring.Guru): the three roles and the nested-class and interface variants for keeping snapshots opaque.
- Implementing Undo History (Redux docs): past, present and future arrays of immutable states, a memento history with structural sharing.
- Understanding Clojure's Persistent Vectors (hypirion): how 32-way trees make a snapshot a single pointer and an update a few node copies.
- Immer documentation: producing structurally shared immutable states from code that looks like mutation.
- Redis persistence (Redis docs): how
fork()and copy-on-write give a consistent snapshot of a live dataset, and what it costs. - copy module (Python docs): shallow versus deep copies and the memo dictionary that preserves shared references.