Memento
Captures and externalizes an object's internal state without violating its encapsulation, allowing it to be restored to that state later — the foundation of undo via a complete snapshot.
Intent
Capture an object's internal state in a separate object (Memento), without exposing that state's implementation details to the outside world. The Memento can be stored and later used to restore the object to the captured state — implementing undo, checkpoints, transactions, and state history.
Cataloged by the GoF (1994) as a behavioral pattern, Memento preserves the Originator's encapsulation: whoever stores the Memento (the Caretaker) doesn't know what's inside it — it just keeps it and hands it back. Only the Originator knows the state's format and knows how to create and restore a Memento. This distinguishes the pattern from a simple public serialization of the state: the Memento's interface is opaque to any object other than the Originator that created it.
Problem
Continuing the text editor domain from the Command pattern: we want to implement undo. The naive approach is to publicly expose the editor's state so an external object can save it:
// Naive approach — violates encapsulation:
class Editor {
content: string = ''; // public so the Caretaker can save it
cursorPos: number = 0; // public so the Caretaker can save it
selection: [number, number] | null = null; // public
}
// The Caretaker accesses and knows the Editor's internals — strong coupling.
// If the Editor changes its fields, the Caretaker breaks.
const savedState = {
content: editor.content,
cursorPos: editor.cursorPos,
selection: editor.selection,
};
The problem is twofold: the Caretaker (whoever stores the state) needs to know the internal details of the Editor (whoever owns the state), creating strong coupling. Any internal refactoring of the Editor — renaming a field, changing the selection's structure — breaks the Caretaker. Memento eliminates this: the Editor produces an opaque object with its state, and the Caretaker just stores it — without inspecting it.
Critical distinction: Memento vs Command with undo
Both patterns implement undo, but with different philosophies:
-
Command with undo: each Command encapsulates a
specific action and knows how to revert it incrementally. An
InsertTextCommandknows how to delete exactly the text it inserted. Undo is precise but requires each Command to capture enough state for its own reversal. - Memento: captures the complete state of the Originator in a snapshot. Undo restores the entire state — it doesn't undo a specific action, it returns the object to an earlier point in time. Simpler to implement for objects with rich, interdependent state, but more costly in memory (one snapshot per restoration point).
The two patterns are complementary: a Command can use Memento as
its state-capture mechanism in its execute(),
delegating the responsibility of "how to save the state" to the
Originator itself.
Solution
Memento organizes the code into three participants:
-
Originator: the object whose state we want to
capture. It creates Mementos via
saveState()and restores its state from a Memento viarestoreState(memento). It's the only one that knows the Memento's internal content. E.g.:Editorwith content, cursor position, and selection. - Memento: an object that stores the snapshot of the Originator's state. Its interface toward the Caretaker is opaque — it doesn't expose getters for the internal state. Only the Originator accesses the content. In TypeScript, this can be modeled with an internal class or private fields with an access method restricted via a nominal type.
-
Caretaker: manages the history of Mementos. It
doesn't inspect or modify the Mementos' content — it just stores
them in order and hands them back to the Originator when undo is
requested. E.g.:
Historywith a stack of Mementos andsave()andundo()methods.
Structure
Editor (Originator)
┌──────────────────────────────────────────────┐
│ - content: string │
│ - cursorPos: number │
│ - selection: [number,number] | null │
│ │
│ + saveState(): EditorSnapshot │
│ → creates a Memento with a copy of the │
│ current state │
│ + restoreState(s: EditorSnapshot): void │
│ → recovers the state from the Memento │
│ + (editing operations) │
└──────────────────────────────────────────────┘
creates and restores ▲ ▼ stored by
│ │
EditorSnapshot (Memento) — opaque interface toward the Caretaker
┌──────────────────────────────────────────────┐
│ (private fields — only the Editor accesses) │
│ - content: string │
│ - cursorPos: number │
│ - selection: [number,number] | null │
│ │
│ (no public getters — opaque to the Caretaker)│
└──────────────────────────────────────────────┘
▲ keeps and hands back
│
History (Caretaker)
┌──────────────────────────────────────────────┐
│ - stack: EditorSnapshot[] │
│ │
│ + save(originator: Editor): void │
│ → stack.push(editor.saveState()) │
│ + undo(originator: Editor): void │
│ → editor.restoreState(stack.pop()) │
│ │
│ !! Never inspects the Memento's content !! │
└──────────────────────────────────────────────┘
Comparing approaches to undo:
Command + incremental undo Memento + snapshot
───────────────────────────── ──────────────────────────────
Each Command knows its own undo Originator knows its own state
Memory: proportional to the action Memory: proportional to the state
Precise for atomic actions Simple for complex state
More boilerplate per action type Less code, more memory
Code examples
Example 1 — Text editor with snapshot-based undo (Memento)
The same domain as Command, but now undo captures the editor's
complete state in a snapshot. Note how History (the
Caretaker) never accesses any internal data of
EditorSnapshot — it just stores it and hands it back.
The comparison with Command is made explicit in the comments.
// ── Memento — opaque snapshot of the Editor ───────────────────
// The interface exposed to the Caretaker is opaque: no public getters.
// Only the Editor accesses the fields via a package-restricted method (friend pattern).
class EditorSnapshot {
// Readonly fields — immutable after the snapshot is created.
constructor(
private readonly content: string,
private readonly cursorPos: number,
private readonly selection: readonly [number, number] | null
) {}
// Restricted method: in practice only the Editor calls it.
// In languages with friend/package-private, this would be inaccessible to the Caretaker.
_recover(): {
content: string;
cursorPos: number;
selection: readonly [number, number] | null;
} {
return {
content: this.content,
cursorPos: this.cursorPos,
selection: this.selection ? [...this.selection] : null,
};
}
}
// ── Originator — creates and restores Mementos ────────────────
class Editor {
private content: string = '';
private cursorPos: number = 0;
private selection: [number, number] | null = null;
// Editing operations
insert(text: string): void {
this.content =
this.content.slice(0, this.cursorPos) +
text +
this.content.slice(this.cursorPos);
this.cursorPos += text.length;
}
deletePrevious(): void {
if (this.cursorPos === 0) return;
this.content =
this.content.slice(0, this.cursorPos - 1) +
this.content.slice(this.cursorPos);
this.cursorPos--;
}
moveCursor(pos: number): void {
this.cursorPos = Math.max(0, Math.min(pos, this.content.length));
}
selectRange(start: number, end: number): void {
this.selection = [start, end];
}
// Creates a snapshot of the COMPLETE state
saveState(): EditorSnapshot {
return new EditorSnapshot(
this.content,
this.cursorPos,
this.selection ? [...this.selection] : null
);
}
// Restores from a snapshot
restoreState(snapshot: EditorSnapshot): void {
const state = snapshot._recover();
this.content = state.content;
this.cursorPos = state.cursorPos;
this.selection = state.selection ? [...state.selection] : null;
}
toString(): string {
const sel = this.selection
? ` [sel ${this.selection[0]}-${this.selection[1]}]`
: '';
return `"${this.content}" cursor=${this.cursorPos}${sel}`;
}
}
// ── Caretaker — stores the history of snapshots ────────────────
class History {
private readonly stack: EditorSnapshot[] = [];
// Saves the editor's current state onto the stack
save(editor: Editor): void {
this.stack.push(editor.saveState());
}
// Restores the last saved state
undo(editor: Editor): boolean {
const snapshot = this.stack.pop();
if (!snapshot) return false;
// The Caretaker hands the Memento back to the Originator — never inspects it.
editor.restoreState(snapshot);
return true;
}
size(): number { return this.stack.length; }
}
// ── Usage ────────────────────────────────────────────────────
const editor = new Editor();
const history = new History();
// State 1: empty
history.save(editor);
console.log(`[0] ${editor}`); // "" cursor=0
editor.insert('Hi');
history.save(editor);
console.log(`[1] ${editor}`); // "Hi" cursor=2
editor.insert(' there');
history.save(editor);
console.log(`[2] ${editor}`); // "Hi there" cursor=8
editor.selectRange(3, 8);
history.save(editor);
console.log(`[3] ${editor}`); // "Hi there" cursor=8 [sel 3-8]
// Undo: back to state 3 (before the selection)
history.undo(editor);
console.log(`Undo → ${editor}`); // "Hi there" cursor=8
// Undo: back to state 2 (before " there")
history.undo(editor);
console.log(`Undo → ${editor}`); // "Hi" cursor=2
// Undo: back to state 1 (empty)
history.undo(editor);
console.log(`Undo → ${editor}`); // "" cursor=0
console.log(`Remaining snapshots: ${history.size()}`); // 0
<?php
// ── Memento — opaque snapshot of the Editor ───────────────────
class EditorSnapshot
{
public function __construct(
private readonly string $content,
private readonly int $cursorPos,
private readonly ?array $selection // [int, int] | null
) {}
/**
* Recovers the state — only the Editor should call this method.
* @return array{content: string, cursorPos: int, selection: ?array}
*/
public function _recover(): array
{
return [
'content' => $this->content,
'cursorPos' => $this->cursorPos,
'selection' => $this->selection,
];
}
}
// ── Originator — creates and restores Mementos ────────────────
class Editor
{
private string $content = '';
private int $cursorPos = 0;
private ?array $selection = null; // [int, int] | null
public function insert(string $text): void
{
$this->content =
substr($this->content, 0, $this->cursorPos)
. $text
. substr($this->content, $this->cursorPos);
$this->cursorPos += strlen($text);
}
public function deletePrevious(): void
{
if ($this->cursorPos === 0) return;
$this->content =
substr($this->content, 0, $this->cursorPos - 1)
. substr($this->content, $this->cursorPos);
$this->cursorPos--;
}
public function moveCursor(int $pos): void
{
$this->cursorPos = max(0, min($pos, strlen($this->content)));
}
public function selectRange(int $start, int $end): void
{
$this->selection = [$start, $end];
}
// Creates a snapshot of the COMPLETE state
public function saveState(): EditorSnapshot
{
return new EditorSnapshot(
$this->content,
$this->cursorPos,
$this->selection
);
}
// Restores from a snapshot
public function restoreState(EditorSnapshot $snapshot): void
{
$state = $snapshot->_recover();
$this->content = $state['content'];
$this->cursorPos = $state['cursorPos'];
$this->selection = $state['selection'];
}
public function __toString(): string
{
$sel = $this->selection
? " [sel {$this->selection[0]}-{$this->selection[1]}]"
: '';
return "\"{$this->content}\" cursor={$this->cursorPos}{$sel}";
}
}
// ── Caretaker — stores the history of snapshots ────────────────
class History
{
/** @var EditorSnapshot[] */
private array $stack = [];
public function save(Editor $editor): void
{
$this->stack[] = $editor->saveState();
}
public function undo(Editor $editor): bool
{
$snapshot = array_pop($this->stack);
if ($snapshot === null) return false;
// The Caretaker hands the Memento back to the Originator — never inspects it.
$editor->restoreState($snapshot);
return true;
}
public function size(): int
{
return count($this->stack);
}
}
// ── Usage ────────────────────────────────────────────────────
$editor = new Editor();
$history = new History();
$history->save($editor);
echo "[0] {$editor}\n"; // "" cursor=0
$editor->insert('Hi');
$history->save($editor);
echo "[1] {$editor}\n"; // "Hi" cursor=2
$editor->insert(' there');
$history->save($editor);
echo "[2] {$editor}\n"; // "Hi there" cursor=8
$editor->selectRange(3, 8);
$history->save($editor);
echo "[3] {$editor}\n"; // "Hi there" cursor=8 [sel 3-8]
$history->undo($editor);
echo "Undo → {$editor}\n"; // "Hi there" cursor=8
$history->undo($editor);
echo "Undo → {$editor}\n"; // "Hi" cursor=2
$history->undo($editor);
echo "Undo → {$editor}\n"; // "" cursor=0
echo "Remaining snapshots: " . $history->size() . "\n"; // 0
Example 2 — App configuration with named checkpoints
Memento isn't limited to editors. A Caretaker with support for named checkpoints (instead of an anonymous stack) allows saving and restoring states with clear semantics — like "factory settings" vs "user settings" — without exposing the configuration object's internal fields.
// ── Configuration Memento ─────────────────────────────────────
class ConfigSnapshot {
constructor(
private readonly data: Readonly<Record<string, unknown>>
) {}
_recover(): Record<string, unknown> {
return { ...this.data }; // defensive copy
}
}
// ── Originator ────────────────────────────────────────────────
class AppConfig {
private data: Record<string, unknown> = {
theme: 'light',
language: 'en-US',
fontSize: 14,
notifications: true,
};
set(key: string, value: unknown): void {
this.data[key] = value;
}
get(key: string): unknown {
return this.data[key];
}
saveState(): ConfigSnapshot {
return new ConfigSnapshot({ ...this.data });
}
restoreState(snapshot: ConfigSnapshot): void {
this.data = snapshot._recover();
}
show(): void {
console.log(JSON.stringify(this.data, null, 2));
}
}
// ── Caretaker with named checkpoints ──────────────────────────
class CheckpointManager {
private readonly checkpoints = new Map<string, ConfigSnapshot>();
save(name: string, config: AppConfig): void {
this.checkpoints.set(name, config.saveState());
console.log(`Checkpoint "${name}" saved.`);
}
restore(name: string, config: AppConfig): boolean {
const snapshot = this.checkpoints.get(name);
if (!snapshot) {
console.warn(`Checkpoint "${name}" not found.`);
return false;
}
config.restoreState(snapshot);
console.log(`Checkpoint "${name}" restored.`);
return true;
}
list(): string[] {
return [...this.checkpoints.keys()];
}
}
// ── Usage ────────────────────────────────────────────────────
const config = new AppConfig();
const manager = new CheckpointManager();
// Saves the factory state
manager.save('factory', config);
// User customizes
config.set('theme', 'dark');
config.set('fontSize', 18);
config.set('language', 'pt-BR');
manager.save('user', config);
// More adjustments — without saving
config.set('notifications', false);
config.set('fontSize', 22);
console.log('\nCurrent state:');
config.show();
// { theme: 'dark', language: 'pt-BR', fontSize: 22, notifications: false }
// Restores the user's configuration
manager.restore('user', config);
config.show();
// { theme: 'dark', language: 'pt-BR', fontSize: 18, notifications: true }
// Restores factory settings
manager.restore('factory', config);
config.show();
// { theme: 'light', language: 'en-US', fontSize: 14, notifications: true }
console.log('Available checkpoints:', manager.list());
// ['factory', 'user']
<?php
// ── Configuration Memento ─────────────────────────────────────
class ConfigSnapshot
{
public function __construct(
private readonly array $data
) {}
/** @return array<string, mixed> */
public function _recover(): array
{
return $this->data; // PHP arrays are copied by value
}
}
// ── Originator ────────────────────────────────────────────────
class AppConfig
{
/** @var array<string, mixed> */
private array $data = [
'theme' => 'light',
'language' => 'en-US',
'fontSize' => 14,
'notifications' => true,
];
public function set(string $key, mixed $value): void
{
$this->data[$key] = $value;
}
public function get(string $key): mixed
{
return $this->data[$key] ?? null;
}
public function saveState(): ConfigSnapshot
{
return new ConfigSnapshot($this->data);
}
public function restoreState(ConfigSnapshot $snapshot): void
{
$this->data = $snapshot->_recover();
}
public function show(): void
{
echo json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
}
}
// ── Caretaker with named checkpoints ──────────────────────────
class CheckpointManager
{
/** @var array<string, ConfigSnapshot> */
private array $checkpoints = [];
public function save(string $name, AppConfig $config): void
{
$this->checkpoints[$name] = $config->saveState();
echo "Checkpoint \"{$name}\" saved.\n";
}
public function restore(string $name, AppConfig $config): bool
{
if (!isset($this->checkpoints[$name])) {
echo "Checkpoint \"{$name}\" not found.\n";
return false;
}
$config->restoreState($this->checkpoints[$name]);
echo "Checkpoint \"{$name}\" restored.\n";
return true;
}
/** @return string[] */
public function list(): array
{
return array_keys($this->checkpoints);
}
}
// ── Usage ────────────────────────────────────────────────────
$config = new AppConfig();
$manager = new CheckpointManager();
$manager->save('factory', $config);
$config->set('theme', 'dark');
$config->set('fontSize', 18);
$config->set('language', 'pt-BR');
$manager->save('user', $config);
$config->set('notifications', false);
$config->set('fontSize', 22);
echo "\nCurrent state:\n";
$config->show();
$manager->restore('user', $config);
$config->show();
$manager->restore('factory', $config);
$config->show();
echo "Checkpoints: " . implode(', ', $manager->list()) . "\n";
// factory, user
When to use
- When you need snapshot-based undo/redo: the pattern's central use case. If the Originator's state is compact enough or the undo depth is limited, Memento is simpler to implement than Command with incremental undo for every operation type.
- When the object's state is rich and interdependent: if multiple fields need to be restored together to guarantee consistency (e.g.: content + cursor + selection in the editor), a complete snapshot is safer than reverting field by field.
- When you need checkpoints or transactions: saving the state before a potentially destructive operation and restoring it on failure — without exposing the object's internals to whoever manages it.
- When the Originator's encapsulation must be preserved: the Caretaker needs to keep the state but shouldn't know its internal structure. Memento creates the necessary abstraction barrier.
When to avoid
- When the Originator's state is large: every snapshot is a copy of the complete state. For objects with megabytes of data, keeping a history of snapshots can exhaust memory. Consider incremental snapshots, compression, or Command with incremental undo.
- When undo depth is unlimited and the object is large: the combination of large state + many undo levels makes Memento prohibitive in memory. Set a maximum number of snapshots in the Caretaker.
- When dynamic languages make encapsulation trivial to break: in PHP and JavaScript, the Memento's encapsulation is conventional, not enforced by the compiler. If the Caretaker can access the Memento's fields via reflection, the pattern's conceptual barrier loses its strength.
Pros and cons
Pros
- Preserves the Originator's encapsulation — the Caretaker doesn't know the internal structure of the state.
- Simplifies the Originator: the undo logic isn't scattered across every operation — it's centralized in saveState/restoreState.
- Allows multi-level undo and named checkpoints with the same mechanism.
- The Caretaker can limit the history's depth by discarding old Mementos — without changing the Originator.
Cons
- High memory usage for Originators with large state — every snapshot is a complete copy.
- The Memento's encapsulation is hard to enforce in languages without support for inner classes or packages (like TypeScript and PHP) — it's conventional, not compiler-enforced.
- If the Originator changes its internal structure, existing Mementos' format can become incompatible (a versioning problem).
- Requires a defensive copy of the state to prevent the Memento from sharing mutable references with the Originator.
Common pitfalls
1. A Caretaker that inspects the Memento (violates encapsulation)
The pattern's fundamental mistake: the Caretaker calls getters on the Memento to read fields, display information, or make decisions. This creates coupling between the Caretaker and the Originator's internal structure — exactly what the pattern aims to avoid. The Caretaker should treat the Memento as a black box: receive it, store it, hand it back. Nothing more.
Warning sign: if the Caretaker has an
if (snapshot.getContent().length > 0) somewhere,
encapsulation has been violated.
2. Shared mutable references (a shallow snapshot)
The most critical technical pitfall: saveState()
copies references instead of values. If the Originator's state
contains mutable objects or arrays and the Memento keeps the
reference (not a deep copy), the snapshot will reflect later
changes — making it useless for restoration. Golden rule: the
Memento must be immutable and contain only copies of the data,
never the original references.
// WRONG: shared reference — the snapshot changes along with the Originator
class EditorSnapshot {
constructor(readonly data: string[]) {} // reference!
}
const snap = new EditorSnapshot(editor.lines); // editor.lines and snap.data point to the same array
editor.lines.push('new line'); // corrupted the snapshot!
// CORRECT: defensive copy
class EditorSnapshot {
constructor(readonly data: readonly string[]) {}
}
const snap = new EditorSnapshot([...editor.lines]); // copy — independent
3. Incompatible Mementos after refactoring (versioning)
If the Originator is serialized for persistence (game save files, user sessions) and the Memento's format changes with a refactoring, old snapshots may become unrestorable. Consider including a version number in the Memento and implementing migration when the format changes — especially if Mementos are persisted beyond the current session.
4. An unbounded history
A Caretaker that accepts unlimited Mementos can gradually exhaust memory. Set a maximum limit (e.g.: 50 snapshots) on the Caretaker and discard the oldest ones once the limit is reached. This is a decision made by the Caretaker — the Originator and the Memento don't need to know about it.
Related patterns
Memento interacts with behavioral patterns that also deal with action history and state management:
Command is Memento's natural and most frequent
complement. The critical distinction: Command undoes a specific
action incrementally — each Command knows exactly how to revert
what it did. Memento restores a complete snapshot of the state —
it doesn't undo an action, it returns the object to an earlier
point in time. The two are complementary: a Command can use
originator.saveState() in its execute()
and originator.restoreState(snapshot) in its
undo(), delegating the responsibility of serializing
itself to the Originator — without the Command needing to know the
Receiver's internal fields. State also manages an
object's state, but its focus is representing discrete states with
explicit transitions, not capturing and restoring arbitrary
states. Memento can complement State when it's necessary to return
to a prior state after a transition — the Memento stores the
snapshot of the StateContext before the transition.