Loading content...
The Memento Pattern's elegance lies in how it distributes responsibilities among three distinct participants. Each has a carefully scoped role that, together with the others, creates a robust state preservation system while maintaining clean architectural boundaries.
In this deep dive, we'll examine each participant's responsibilities, design considerations, variations, and how they collaborate. We'll see how subtle changes in their implementation can optimize for different concerns: memory efficiency, thread safety, persistence, or debugging.
By the end of this page, you will understand: • The Originator's dual role as state owner and memento factory • How Memento design affects encapsulation and efficiency • Caretaker strategies for different history management needs • Coordination patterns between participants • Common mistakes and how to avoid them
The Originator is the object whose state needs preservation. It serves two critical roles:
This dual responsibility ensures that all state management knowledge resides in one place, following the Single Responsibility Principle at a deeper level—the originator is solely responsible for its own state.
Design considerations for the Originator:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
/** * A well-designed Originator with comprehensive state management */class DocumentEditor { // === Core State (Always Preserved) === private text: string; private formatRanges: FormatRange[]; private metadata: DocumentMetadata; // === Derived State (Can Be Recomputed) === private wordCount: number; // Derived from text private searchIndex: SearchIndex; // Built from text // === Transient State (Never Preserved) === private isLoading: boolean; private pendingNetworkRequests: Set<string>; private undoLevel: number; // Managed by Caretaker, not Memento // === Shared References (Preserve Reference, Not Copy) === private sharedStylesheet: Stylesheet; // Shared across documents private pluginRegistry: PluginRegistry; // Application-level /** * Create a memento - key decisions: * 1. Include core state (text, formatRanges, metadata) * 2. Exclude derived state (can recompute) * 3. Exclude transient state (irrelevant to document content) * 4. Preserve reference to shared objects, not copies */ createMemento(): DocumentMemento { return new DocumentMemento( // Deep copy core state this.text, this.formatRanges.map(r => ({ ...r })), // Clone array of objects { ...this.metadata }, // Shallow clone is enough for flat object // Keep reference to shared styles (don't copy) this.sharedStylesheet ); } /** * Restore from memento - key decisions: * 1. Restore core state from memento * 2. Recompute derived state after restoration * 3. Don't touch transient state * 4. Validate invariants after restoration */ restore(memento: DocumentMemento): void { // Restore core state this.text = memento.getText(); this.formatRanges = memento.getFormatRanges(); this.metadata = memento.getMetadata(); this.sharedStylesheet = memento.getStylesheet(); // Recompute derived state this.wordCount = this.computeWordCount(); this.searchIndex = this.buildSearchIndex(); // Validate invariants this.validateState(); } private computeWordCount(): number { return this.text.split(/\s+/).filter(w => w.length > 0).length; } private buildSearchIndex(): SearchIndex { // Expensive operation - worth recomputing vs storing return SearchIndex.buildFrom(this.text); } private validateState(): void { // Ensure format ranges don't exceed text length for (const range of this.formatRanges) { if (range.end > this.text.length) { throw new Error('Invalid state: format range exceeds text'); } } }} interface FormatRange { start: number; end: number; format: TextFormat;} interface DocumentMetadata { title: string; author: string; lastModified: Date;} interface TextFormat { bold?: boolean; italic?: boolean; fontSize?: number;} // Placeholder types for illustrationtype SearchIndex = any;type Stylesheet = any;type PluginRegistry = any;Before implementing state preservation, classify all state: Core (must preserve), Derived (can recompute), Transient (ignore), and Shared (preserve reference). This classification determines memento content and restoration logic.
The Memento is the cornerstone of the pattern—it stores state in a way that preserves encapsulation. Its design is subtle because it must present two different interfaces: a wide one to the Originator and a narrow one to everyone else.
Key properties of a well-designed Memento:
Memento implementation strategies:
There are several ways to implement the wide/narrow interface pattern, depending on your language and requirements:
The Memento is a private nested class of the Originator. This is the classic GoF approach.
12345678910111213141516171819202122232425262728293031323334353637383940414243
// Java: Nested class provides natural encapsulationpublic class TextEditor { private String content; private int cursorPosition; // Nested class - private constructor, package-private access public static class EditorMemento { private final String content; private final int cursorPosition; private final Instant timestamp; // Package-private constructor - only TextEditor can create EditorMemento(String content, int cursorPosition) { this.content = content; this.cursorPosition = cursorPosition; this.timestamp = Instant.now(); } // Narrow interface - accessible to all public Instant getTimestamp() { return timestamp; } // Wide interface - package-private, only TextEditor uses String getContent() { return content; } int getCursorPosition() { return cursorPosition; } } public EditorMemento createMemento() { return new EditorMemento(content, cursorPosition); } public void restore(EditorMemento memento) { // Has access to package-private methods this.content = memento.getContent(); this.cursorPosition = memento.getCursorPosition(); }}The Caretaker is responsible for managing mementos—storing them, organizing them, and returning them when needed. It decides when to save state, how long to keep it, and when to restore, but never what state means or how restoration happens.
Caretaker strategies for different use cases:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
// ============================================================// Strategy 1: Stack-based Undo/Redo// Classic implementation for text editors, drawing apps// ============================================================class UndoRedoManager<T extends IMemento> { private undoStack: T[] = []; private redoStack: T[] = []; private readonly maxSize: number; constructor(maxSize: number = 100) { this.maxSize = maxSize; } /** * Save current state before making changes. * Clears redo stack (new action invalidates redo history). */ save(memento: T): void { this.undoStack.push(memento); this.redoStack = []; // Clear redo on new action // Limit memory usage while (this.undoStack.length > this.maxSize) { this.undoStack.shift(); // Remove oldest } } /** * Get memento for undo operation. * Caller should save current state to redo stack. */ undo(currentMemento: T): T | null { if (this.undoStack.length === 0) return null; this.redoStack.push(currentMemento); return this.undoStack.pop()!; } /** * Get memento for redo operation. */ redo(currentMemento: T): T | null { if (this.redoStack.length === 0) return null; this.undoStack.push(currentMemento); return this.redoStack.pop()!; } canUndo(): boolean { return this.undoStack.length > 0; } canRedo(): boolean { return this.redoStack.length > 0; }} // ============================================================// Strategy 2: Named Checkpoints// For save points, game saves, database transactions// ============================================================class CheckpointManager<T extends IMemento> { private checkpoints: Map<string, T> = new Map(); private autoCheckpoints: T[] = []; private autoCheckpointInterval: number; constructor(autoCheckpointInterval: number = 10) { this.autoCheckpointInterval = autoCheckpointInterval; } /** * Create a named checkpoint for explicit save points. */ createCheckpoint(name: string, memento: T): void { this.checkpoints.set(name, memento); } /** * Auto-checkpoint based on operation count or time. */ autoCheckpoint(memento: T): void { this.autoCheckpoints.push(memento); // Keep only recent auto-checkpoints while (this.autoCheckpoints.length > this.autoCheckpointInterval) { this.autoCheckpoints.shift(); } } /** * Restore to a named checkpoint. */ restore(name: string): T | null { return this.checkpoints.get(name) || null; } /** * Restore to most recent auto-checkpoint. */ restoreLatest(): T | null { return this.autoCheckpoints.length > 0 ? this.autoCheckpoints[this.autoCheckpoints.length - 1] : null; } listCheckpoints(): string[] { return Array.from(this.checkpoints.keys()); }} // ============================================================// Strategy 3: Version History with Branching// For version control, document history, design tools// ============================================================interface VersionNode<T extends IMemento> { id: string; memento: T; parentId: string | null; timestamp: Date; label?: string;} class VersionHistory<T extends IMemento> { private versions: Map<string, VersionNode<T>> = new Map(); private currentId: string | null = null; private rootId: string | null = null; /** * Add a new version as child of current. * Enables branching history like Git. */ addVersion(memento: T, label?: string): string { const id = crypto.randomUUID(); const node: VersionNode<T> = { id, memento, parentId: this.currentId, timestamp: new Date(), label }; this.versions.set(id, node); if (this.rootId === null) { this.rootId = id; } this.currentId = id; return id; } /** * Navigate to any version in history. * Returns null if version doesn't exist. */ checkout(versionId: string): T | null { const node = this.versions.get(versionId); if (!node) return null; this.currentId = versionId; return node.memento; } /** * Get the path from root to current version. */ getHistoryPath(): VersionNode<T>[] { const path: VersionNode<T>[] = []; let currentNode = this.currentId ? this.versions.get(this.currentId) : null; while (currentNode) { path.unshift(currentNode); currentNode = currentNode.parentId ? this.versions.get(currentNode.parentId) : null; } return path; } /** * Get all branches from a specific version. */ getBranches(versionId: string): VersionNode<T>[] { return Array.from(this.versions.values()) .filter(node => node.parentId === versionId); }} interface IMemento { timestamp: Date; getDescription(): string;}The Caretaker's power lies in controlling when to save and restore. It can implement sophisticated undo models (linear, branching, grouped) without knowing anything about the actual state being preserved.
Understanding how the three participants interact is crucial for implementing the pattern correctly. Let's trace through several common scenarios.
Key interaction principles:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
// Complete example showing all three participants interacting interface IMemento { readonly timestamp: Date; getDescription(): string;} // === MEMENTO ===class GraphicsMemento implements IMemento { readonly timestamp = new Date(); constructor( private readonly layerData: readonly LayerSnapshot[], private readonly selectedLayerId: string | null, private readonly viewportPosition: Readonly<Point> ) {} getDescription(): string { return `${this.layerData.length} layers at ${this.timestamp.toISOString()}`; } // Wide interface - package-private in practice getLayerData(): readonly LayerSnapshot[] { return this.layerData; } getSelectedLayerId(): string | null { return this.selectedLayerId; } getViewportPosition(): Readonly<Point> { return this.viewportPosition; }} // === ORIGINATOR ===class GraphicsCanvas { private layers: Layer[] = []; private selectedLayerId: string | null = null; private viewportPosition: Point = { x: 0, y: 0 }; // Business operations addLayer(layer: Layer): void { /* ... */ } removeLayer(id: string): void { /* ... */ } selectLayer(id: string): void { /* ... */ } pan(delta: Point): void { /* ... */ } // Memento operations createMemento(): GraphicsMemento { return new GraphicsMemento( this.layers.map(l => l.snapshot()), // Deep copy this.selectedLayerId, { ...this.viewportPosition } ); } restore(memento: GraphicsMemento): void { this.layers = memento.getLayerData().map(snap => Layer.fromSnapshot(snap) ); this.selectedLayerId = memento.getSelectedLayerId(); this.viewportPosition = { ...memento.getViewportPosition() }; this.onStateRestored(); // Hook for derived state update } private onStateRestored(): void { // Rebuild derived state, notify observers, etc. }} // === CARETAKER ===class CanvasHistoryManager { private undoStack: IMemento[] = []; private redoStack: IMemento[] = []; /** * Call this BEFORE making changes to the canvas. */ beforeChange(canvas: GraphicsCanvas): void { const memento = canvas.createMemento(); this.undoStack.push(memento); this.redoStack = []; // Limit history if (this.undoStack.length > 50) { this.undoStack.shift(); } } /** * Undo the last change. */ undo(canvas: GraphicsCanvas): boolean { if (this.undoStack.length === 0) return false; // Save current for redo const current = canvas.createMemento(); this.redoStack.push(current); // Restore previous const previous = this.undoStack.pop()! as GraphicsMemento; canvas.restore(previous); return true; } /** * Redo a previously undone change. */ redo(canvas: GraphicsCanvas): boolean { if (this.redoStack.length === 0) return false; // Save current for undo const current = canvas.createMemento(); this.undoStack.push(current); // Restore redo state const redoState = this.redoStack.pop()! as GraphicsMemento; canvas.restore(redoState); return true; }} // === USAGE ===const canvas = new GraphicsCanvas();const history = new CanvasHistoryManager(); // Before any modificationhistory.beforeChange(canvas);canvas.addLayer(new Layer('background')); history.beforeChange(canvas);canvas.addLayer(new Layer('foreground')); // Undo - removes foreground layerhistory.undo(canvas); // Types for illustrationinterface Point { x: number; y: number; }interface LayerSnapshot { id: string; data: unknown; }class Layer { constructor(public name: string) {} snapshot(): LayerSnapshot { return { id: this.name, data: {} }; } static fromSnapshot(snap: LayerSnapshot): Layer { return new Layer(snap.id); }}Even experienced developers can make subtle mistakes when implementing the Memento Pattern. Here are the most common pitfalls and how to avoid them.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
// ❌ MISTAKE 1: Mutable mementoclass BadMemento { constructor(public items: string[]) {} // Mutable array!} const origItems = ['a', 'b'];const memento = new BadMemento(origItems); // Shares reference!origItems.push('c'); // Memento now has 3 items! // ✅ FIX: Deep copyclass GoodMemento { private readonly items: readonly string[]; constructor(items: string[]) { this.items = Object.freeze([...items]); // Copy and freeze }} // ❌ MISTAKE 2: Caretaker reading stateclass BadCaretaker { shouldSave(memento: BadMemento): boolean { // WRONG: Reading internal state return memento.items.length > 0; }} // ✅ FIX: Add to narrow interfaceinterface IMemento { isEmpty(): boolean; // Originator defines this} // ❌ MISTAKE 3: Save after modificationfunction badUndo(editor: Editor, history: History) { editor.type('text'); // Modify first history.push(editor.createMemento()); // Save after - WRONG!} // ✅ FIX: Save before modificationfunction goodUndo(editor: Editor, history: History) { history.push(editor.createMemento()); // Save first editor.type('text'); // Then modify} // ❌ MISTAKE 4: Not updating derived stateclass IncompleteEditor { private text: string = ''; private wordCount: number = 0; // Derived restore(memento: Memento): void { this.text = memento.getText(); // WRONG: wordCount is now stale! }} // ✅ FIX: Update derived state after restoreclass CompleteEditor { private text: string = ''; private wordCount: number = 0; restore(memento: Memento): void { this.text = memento.getText(); this.updateDerivedState(); // Always recompute } private updateDerivedState(): void { this.wordCount = this.text.split(/\s+/).length; }} // Placeholder typestype Editor = any;type History = any[];type Memento = { getText(): string };We've explored the Originator, Memento, and Caretaker in depth—their responsibilities, implementations, interactions, and common pitfalls. Let's consolidate the key insights:
| Participant | Knows | Does Not Know | Creates | Uses |
|---|---|---|---|---|
| Originator | Internal state structure | When/how mementos are stored | Mementos | Memento wide interface |
| Memento | State snapshot (frozen) | Who will use it, when | — | — |
| Caretaker | When to save/restore | State content or structure | History structure | Memento narrow interface |
What's next:
Now that we understand the participants, the next page explores real-world use cases and practical examples—undo/redo systems, save points, transaction rollback, and more.
You now have a deep understanding of the Originator, Memento, and Caretaker roles—their responsibilities, design considerations, interactions, and common mistakes to avoid.