Memento captures an object's internal state so it can be restored later, without letting anyone else see what that state contains. The encapsulation clause is the whole pattern; without it you have a public getter and a constructor.
It is the pattern behind undo, checkpoints, transaction rollback and test fixtures — and its correctness rests almost entirely on the copying rules covered under Prototype.
The problem
// The obvious version: expose the internals so the caller can save them
const saved = { text: editor.text, cursor: editor.cursor, selection: editor.selection };
// …later
editor.text = saved.text;
editor.cursor = saved.cursor;
editor.selection = saved.selection;The caller now knows the editor's fields. Adding a field means editing every save site, and forgetting one produces a restore that silently loses state. The editor cannot change its representation without breaking its callers.
The pattern
Three roles, and the naming is worth keeping because it makes the responsibility split obvious.
The originator creates and consumes mementos — it is the only thing that understands their contents. The memento is an opaque token holding the state. The caretaker holds mementos and decides when to restore, without ever looking inside.
class Editor {
#text = ''; #cursor = 0; #selection = null;
save() { return new EditorMemento(this.#text, this.#cursor, this.#selection); }
restore(memento) {
({ text: this.#text, cursor: this.#cursor, selection: this.#selection } = memento.state());
}
}
class History { // the caretaker: holds, never inspects
#stack = [];
push(memento) { this.#stack.push(memento); }
pop() { return this.#stack.pop(); }
}Diagram, as text
- Editor originator leads to Memento opaque state (save())
- Memento opaque state leads to History caretaker
- History caretaker leads to Memento (pop())
- Memento leads to Editor originator (restore())
- History caretaker leads to Memento opaque state (cannot read the contents)
Adding a field to the editor changes save and restore and nothing else. The history has no opinion about what a state is.
The copy must be deep, and this is where it fails
A memento holding a reference to a mutable structure the originator still uses is not a snapshot — it is a view of the live object.
save() { return new EditorMemento(this.#lines); } // shares the arrayType a character, and the "saved" state changes with it. Undo restores the current state and appears to do nothing. This is the aliasing failure from Prototype, and it is the single most common Memento bug because the code looks obviously correct.
Freeze what you capture, and copy nested structures — not just their containers. If the state is already immutable, the whole problem disappears, which is the strongest argument for immutable domain models in editors and simulators.
Memory is the practical constraint
A full snapshot per keystroke is unworkable for a large document. Three mitigations, in increasing order of effort:
Bound the history. Keep the last 100 states and drop the rest. Simple, and it makes undo depth a product decision rather than an accident.
Snapshot on meaningful boundaries. Group keystrokes into one memento per word, pause or command, rather than per character. This also matches what users expect undo to do.
Store deltas rather than states. Keep the change and reverse it — which is Command with an inverse, not Memento. The two are frequently combined: periodic full mementos as checkpoints, with commands in between, so restoring means loading the nearest checkpoint and replaying forward. That is the same structure event-sourced systems use for snapshots.
Where it appears beyond undo
Transaction rollback. Capture before the attempt, restore if it fails. This is Memento at the object level, and it has the same side-effect caveat.
Test fixtures. Capture a valid object once, restore per test. Faster than rebuilding and less brittle than mutating a shared instance — provided the restore really is a deep copy, or tests contaminate one another exactly as described above.
Simulation and game state. Save points, replays, what-if branches from a captured state.
Optimistic concurrency. Keep the state read, attempt the change, restore on conflict.
Check your understanding
Common mistakes
Shallow capture. A memento holding live references is a view, not a snapshot, and undo silently does nothing.
Copying containers but not contents. One level is not enough, and the resulting bug is subtler than no copy at all.
Letting the caretaker read the memento. The encapsulation was the pattern; without it, every save site depends on the state's shape.
Treating restore as undo of side effects. Emails sent and files written are not reversed by restoring fields.
Unbounded history. Memory grows with session length, and long-lived editors leak.
Snapshotting per keystroke. Wasteful, and it does not match what users mean by undo.
Forgetting a new field in save. The restore then silently loses it — keep save and restore adjacent so they are read together.
Practice
Design undo for a vector drawing application: shapes, groups, layers, styles, with documents up to 50 MB. Choose your approach and justify the memory behaviour.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Consider what the inverse of each operation looks like before deciding.
Show a strong answer
Start from the operations, because that decides everything else. In a drawing app most operations have clean inverses: move by a delta reverses by the negative delta, style changes reverse to the previous value, adding a shape reverses by removing it. Those are cheap commands and memory is proportional to edits, not to the 50 MB document.
The awkward ones are where I would concentrate:
- Delete. The inverse must restore the shape and its position in the z-order and its group membership. That is a small memento attached to the command, not a computable inverse — the classic hybrid.
- Group and ungroup. Ungrouping loses the grouping structure, so the inverse needs it captured.
- Boolean operations and flatten. These are genuinely lossy: two shapes become one and the originals cannot be derived. Capture the inputs.
- Filters and rasterisation. Not reversible at all; capture the affected region before.
So the design is command-based with per-command mementos where the inverse is lossy — which is the honest answer for this domain rather than picking one pattern.
Memory behaviour, which is the part the question is really asking about:
- A move or style change costs bytes. A thousand of them cost kilobytes, so a deep history is free for the common case.
- A lossy operation costs the affected region, not the document. Capturing a filter's input region rather than the whole canvas is the difference between a workable history and one that exhausts memory in twenty operations.
- Bound the history by total bytes, not by count. A hundred moves and a hundred filters are wildly different, and a count-based limit gets one of them wrong. A byte budget with oldest-first eviction handles both.
- Never snapshot the whole document per operation. At 50 MB, ten operations is half a gigabyte.
Two things I would insist on:
- Coalesce continuous operations. A drag is hundreds of move events and should be one undo step, recorded on mouse-up. This is a correctness issue for the user experience, not an optimisation.
- Deep-copy or freeze anything captured. A memento holding a live reference to a shape's point array is the bug from this topic, and in a drawing app it appears as "undo did nothing" after the user has moved on.
What I would check with the product owner: whether undo must survive a crash. If yes, the history has to be persisted, which pushes towards a serialisable command log — and that constraint would justify making every operation carry its inverse explicitly rather than relying on in-memory mementos.
A test suite restores a shared fixture object from a memento before each test. Tests still contaminate one another. Diagnose the likely causes and propose a fix.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Restoring from a memento only helps if both the capture and the restore are complete.
Show a strong answer
There are three candidate causes and they need distinguishing before fixing anything.
Cause one: the memento is shallow. The captured state holds references to nested structures the fixture still uses, so a test that mutates a nested object mutates the memento too. Restoring then reinstates the contaminated state — the restore runs and changes nothing. This is the most likely cause, and the giveaway is that contamination involves nested data rather than top-level fields.
Cause two: save misses a field. A field added later was never included in the capture, so restore leaves whatever the previous test set. The giveaway here is that contamination is confined to one specific attribute, and it points at the maintenance hazard that save and restore must be updated together.
Cause three: the state is not all in the fixture. Tests may be contaminating something the fixture does not own — a module-level cache, a clock, a registry, a singleton. No amount of correct restoring fixes this, and it is worth checking early because it invalidates the whole approach.
How I would tell them apart quickly:
- Capture, mutate deeply, then compare the memento to a reference copy. If the memento changed, it is cause one.
- Capture, mutate every field, restore, and diff the fixture against a freshly built one. Any difference names the missing field — cause two.
- Run the failing pair with the fixture rebuilt from scratch rather than restored. If it still fails, the state is elsewhere — cause three.
The fix I would ship, regardless of which cause it turns out to be: build a fresh fixture per test.
- Restoring from a memento is an optimisation, and it is buying very little here — object construction in a test is microseconds, and the suite is paying for that saving with a whole class of intermittent, order-dependent failures that cost hours each time.
- A fresh object per test cannot be contaminated by construction, which is a much stronger property than "we believe the restore is complete".
- If construction genuinely is expensive — it loads a file, hits a database — a test data builder gives the same convenience with no shared state, and it makes each test state what it cares about.
What I would do if measurement showed rebuilding really is too slow: keep the memento, but make the state immutable so the shallow-copy question cannot arise, and add a test asserting that a capture-mutate-restore cycle returns the object to an equal-but-not-identical state. That test is the one that would have caught this months ago.
Key takeaways
- Memento captures state opaquely, so the caretaker holds it without depending on its shape and the originator can change representation freely.
- The copy must be deep. A memento holding live references is a view, and undo silently does nothing.
- Immutable state removes the whole problem — the strongest argument for immutable models in editors and simulators.
- Restore reverses internal state, not side effects. Emails sent and files written need compensation.
- Bound the history, ideally by total bytes rather than count, and snapshot on meaningful boundaries rather than per keystroke.
- Commands with inverses cost memory proportional to changes; mementos cost state size. Real systems combine them, with checkpoints and replay.
- Keep
saveandrestoreadjacent, so a new field cannot be added to one and forgotten in the other.
Further reading
- Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
- Joshua Bloch, Effective Java, third edition (Addison-Wesley, 2018) — defensive copying and immutability.
- Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley, 2002) — snapshots and optimistic offline locking.
- Nat Pryce and Steve Freeman, Growing Object-Oriented Software, Guided by Tests (Addison-Wesley, 2009) — test data builders instead of shared fixtures.
- Chris Okasaki, Purely Functional Data Structures (Cambridge University Press, 1998) — persistent structures that make snapshots nearly free.