Loading learning content...
The Memento Pattern solves the state preservation dilemma with an elegant insight: the object itself creates a snapshot of its state, packages it in an opaque container, and hands it to external code for safekeeping. When restoration is needed, the external code returns the container, and the object—and only the object—unpacks it.
This deceptively simple idea satisfies all our requirements:
The pattern achieves this through three carefully defined roles that we'll explore in depth.
By the end of this page, you will understand: • The Memento Pattern's structure and participants • How encapsulation is maintained through interface design • Creating and restoring from memento objects • Wide vs narrow interface strategies • Implementation patterns in TypeScript and other languages
The Memento Pattern defines three primary participants, each with a specific responsibility:
Originator: The object whose state needs preservation. It creates mementos containing snapshots of its current state and can restore its state from mementos.
Memento: An opaque object that stores the Originator's state. The Memento has a "narrow interface" for external code (essentially nothing) and a "wide interface" for the Originator (full state access).
Caretaker: Manages the collection of mementos. It requests mementos from the Originator, stores them, and returns them for restoration. Crucially, it never examines memento contents.
The collaboration flow:
The brilliance of the pattern lies in access control. The Memento exposes its state only to the Originator. In languages with friend classes or nested classes, this is enforced at compile time. In others, it's maintained by convention and careful interface design.
Let's implement the Memento Pattern for a text editor—a classic use case that demonstrates all aspects of the pattern. We'll build this incrementally, showing how each component contributes to the overall design.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
// ============================================================// MEMENTO: The state snapshot container// ============================================================ /** * The Memento stores a snapshot of the Editor's internal state. * * Design decisions: * - State is readonly to ensure immutability * - Constructor is accessible only within the module (by convention) * - Provides getters for the Originator to access state */class EditorMemento { // State fields - match what the Editor needs to preserve private readonly content: string; private readonly cursorPosition: number; private readonly selectionStart: number | null; private readonly selectionEnd: number | null; private readonly timestamp: Date; /** * Creates a new memento capturing the given state. * This should only be called by the Editor (Originator). */ constructor( content: string, cursorPosition: number, selectionStart: number | null, selectionEnd: number | null ) { this.content = content; this.cursorPosition = cursorPosition; this.selectionStart = selectionStart; this.selectionEnd = selectionEnd; this.timestamp = new Date(); } // -------------------------------------------------------- // Wide interface: accessible to the Originator // In TypeScript, we rely on module boundaries or convention // -------------------------------------------------------- getContent(): string { return this.content; } getCursorPosition(): number { return this.cursorPosition; } getSelectionStart(): number | null { return this.selectionStart; } getSelectionEnd(): number | null { return this.selectionEnd; } // -------------------------------------------------------- // Narrow interface: what external code can see // -------------------------------------------------------- getTimestamp(): Date { return this.timestamp; } /** * Returns a description without exposing actual content. * This is all the Caretaker should ever use. */ getDescription(): string { const truncated = this.content.length > 20 ? this.content.substring(0, 20) + '...' : this.content; return `Snapshot at ${this.timestamp.toISOString()}: "${truncated}"`; }}Now let's implement the Originator—the Editor itself:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
// ============================================================// ORIGINATOR: The object whose state is preserved// ============================================================ /** * TextEditor is the Originator. It: * - Has internal state that needs preservation * - Creates mementos containing state snapshots * - Restores its state from mementos */class TextEditor { // Internal state - these are the fields we preserve private content: string; private cursorPosition: number; private selectionStart: number | null; private selectionEnd: number | null; constructor() { this.content = ''; this.cursorPosition = 0; this.selectionStart = null; this.selectionEnd = null; } // -------------------------------------------------------- // Business operations that modify state // -------------------------------------------------------- type(text: string): void { // Insert text at cursor, replace selection if any if (this.selectionStart !== null && this.selectionEnd !== null) { const before = this.content.substring(0, this.selectionStart); const after = this.content.substring(this.selectionEnd); this.content = before + text + after; this.cursorPosition = this.selectionStart + text.length; this.clearSelection(); } else { const before = this.content.substring(0, this.cursorPosition); const after = this.content.substring(this.cursorPosition); this.content = before + text + after; this.cursorPosition += text.length; } } delete(): void { if (this.selectionStart !== null && this.selectionEnd !== null) { const before = this.content.substring(0, this.selectionStart); const after = this.content.substring(this.selectionEnd); this.content = before + after; this.cursorPosition = this.selectionStart; this.clearSelection(); } else if (this.cursorPosition > 0) { const before = this.content.substring(0, this.cursorPosition - 1); const after = this.content.substring(this.cursorPosition); this.content = before + after; this.cursorPosition--; } } moveCursor(position: number): void { this.cursorPosition = Math.max(0, Math.min(position, this.content.length)); this.clearSelection(); } select(start: number, end: number): void { this.selectionStart = Math.max(0, Math.min(start, this.content.length)); this.selectionEnd = Math.max(0, Math.min(end, this.content.length)); if (this.selectionStart > this.selectionEnd) { [this.selectionStart, this.selectionEnd] = [this.selectionEnd, this.selectionStart]; } } private clearSelection(): void { this.selectionStart = null; this.selectionEnd = null; } // -------------------------------------------------------- // Memento operations - state preservation interface // -------------------------------------------------------- /** * Creates a memento capturing current state. * This is the only way to extract state from the Editor. */ createMemento(): EditorMemento { return new EditorMemento( this.content, this.cursorPosition, this.selectionStart, this.selectionEnd ); } /** * Restores state from a memento. * This is the only way to set state from outside operations. */ restore(memento: EditorMemento): void { this.content = memento.getContent(); this.cursorPosition = memento.getCursorPosition(); this.selectionStart = memento.getSelectionStart(); this.selectionEnd = memento.getSelectionEnd(); } // -------------------------------------------------------- // Read-only accessors for display purposes // -------------------------------------------------------- getContent(): string { return this.content; } getCurrentState(): string { return `Content: "${this.content}" | Cursor: ${this.cursorPosition} | Selection: ${ this.selectionStart !== null ? `[${this.selectionStart}-${this.selectionEnd}]` : 'none' }`; }}Finally, the Caretaker that manages memento storage:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
// ============================================================// CARETAKER: Manages memento storage and retrieval// ============================================================ /** * UndoManager is the Caretaker. It: * - Requests mementos from the Originator * - Stores mementos in appropriate structures (stack for undo/redo) * - Returns mementos to Originator for restoration * - Never examines memento contents (respects narrow interface) */class UndoManager { private undoStack: EditorMemento[] = []; private redoStack: EditorMemento[] = []; private maxHistory: number; constructor(maxHistory: number = 50) { this.maxHistory = maxHistory; } /** * Saves current state before an operation. * Call this BEFORE making changes to the editor. */ saveState(editor: TextEditor): void { const memento = editor.createMemento(); this.undoStack.push(memento); // Limit history size if (this.undoStack.length > this.maxHistory) { this.undoStack.shift(); } // Clear redo stack on new action this.redoStack = []; } /** * Undoes the last operation by restoring previous state. * @returns true if undo was performed, false if no history */ undo(editor: TextEditor): boolean { if (this.undoStack.length === 0) { console.log('Nothing to undo'); return false; } // Save current state to redo stack const currentMemento = editor.createMemento(); this.redoStack.push(currentMemento); // Restore previous state const previousMemento = this.undoStack.pop()!; editor.restore(previousMemento); console.log(`Undo: restored to ${previousMemento.getDescription()}`); return true; } /** * Redoes a previously undone operation. * @returns true if redo was performed, false if nothing to redo */ redo(editor: TextEditor): boolean { if (this.redoStack.length === 0) { console.log('Nothing to redo'); return false; } // Save current state to undo stack const currentMemento = editor.createMemento(); this.undoStack.push(currentMemento); // Restore redo state const redoMemento = this.redoStack.pop()!; editor.restore(redoMemento); console.log(`Redo: restored to ${redoMemento.getDescription()}`); return true; } /** * Shows history without exposing actual content. * Demonstrates the narrow interface in action. */ showHistory(): void { console.log('=== Undo History ==='); this.undoStack.forEach((memento, index) => { // Can only access narrow interface! console.log(` ${index + 1}. ${memento.getDescription()}`); }); console.log(`Redo stack: ${this.redoStack.length} items`); } canUndo(): boolean { return this.undoStack.length > 0; } canRedo(): boolean { return this.redoStack.length > 0; }}Let's see the complete implementation working together. This demonstrates the collaboration between Originator, Memento, and Caretaker:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
// ============================================================// DEMONSTRATION: Complete workflow// ============================================================ function demonstrateMementoPattern(): void { // Create the Originator (TextEditor) and Caretaker (UndoManager) const editor = new TextEditor(); const undoManager = new UndoManager(); console.log('=== Memento Pattern Demo ==='); // --- Step 1: Type some content --- console.log('Step 1: Typing "Hello"'); undoManager.saveState(editor); // Save before change editor.type('Hello'); console.log(` State: ${editor.getCurrentState()}`); // --- Step 2: Add more content --- console.log('Step 2: Typing " World"'); undoManager.saveState(editor); editor.type(' World'); console.log(` State: ${editor.getCurrentState()}`); // --- Step 3: Add punctuation --- console.log('Step 3: Typing "!"'); undoManager.saveState(editor); editor.type('!'); console.log(` State: ${editor.getCurrentState()}`); // --- Step 4: Delete something --- console.log('Step 4: Deleting last character'); undoManager.saveState(editor); editor.delete(); console.log(` State: ${editor.getCurrentState()}`); // --- Show history --- console.log(''); undoManager.showHistory(); // --- Undo operations --- console.log('--- Undo Operations ---'); undoManager.undo(editor); console.log(` State: ${editor.getCurrentState()}`); undoManager.undo(editor); console.log(` State: ${editor.getCurrentState()}`); // --- Redo operation --- console.log('--- Redo Operation ---'); undoManager.redo(editor); console.log(` State: ${editor.getCurrentState()}`); // --- Type after undo (clears redo stack) --- console.log('--- Type after undo ---'); undoManager.saveState(editor); editor.type('!!!'); console.log(` State: ${editor.getCurrentState()}`); console.log(` Can redo: ${undoManager.canRedo()}`); // false} /* Output:=== Memento Pattern Demo === Step 1: Typing "Hello" State: Content: "Hello" | Cursor: 5 | Selection: none Step 2: Typing " World" State: Content: "Hello World" | Cursor: 11 | Selection: none Step 3: Typing "!" State: Content: "Hello World!" | Cursor: 12 | Selection: none Step 4: Deleting last character State: Content: "Hello World" | Cursor: 11 | Selection: none === Undo History === 1. Snapshot at 2024-...: "" 2. Snapshot at 2024-...: "Hello" 3. Snapshot at 2024-...: "Hello World" 4. Snapshot at 2024-...: "Hello World!"Redo stack: 0 items --- Undo Operations ---Undo: restored to Snapshot at ...: "Hello World!" State: Content: "Hello World!" | Cursor: 12 | Selection: noneUndo: restored to Snapshot at ...: "Hello World" State: Content: "Hello World" | Cursor: 11 | Selection: none --- Redo Operation ---Redo: restored to Snapshot at ...: "Hello World!" State: Content: "Hello World!" | Cursor: 12 | Selection: none --- Type after undo --- State: Content: "Hello World!!!!!" | Cursor: 15 | Selection: none Can redo: false*/Notice that the UndoManager never directly accesses editor content, cursor position, or selection. It only interacts through createMemento() and restore(). The internal state structure is completely hidden.
A key concept in the Memento Pattern is the distinction between wide and narrow interfaces. This is how we achieve encapsulation while still allowing state access where needed.
Implementing in different languages:
The mechanism for enforcing wide/narrow interfaces varies by language:
friend classes to grant Originator access to private Memento membersinternal access modifiers within the same assemblyLet's see a more robust TypeScript implementation using Symbol-based privacy:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
// Using Symbol for truly private state access// This prevents external code from accessing state even if they try // This symbol is module-private - external code cannot access itconst MEMENTO_STATE = Symbol('MementoState'); // Narrow interface available to allinterface IMemento { readonly timestamp: Date; getDescription(): string;} // Memento implementation - wide interface is Symbol-protectedclass Memento implements IMemento { readonly timestamp: Date; constructor(private readonly [MEMENTO_STATE]: EditorState) { this.timestamp = new Date(); } getDescription(): string { return `Snapshot at ${this.timestamp.toISOString()}`; } // Only accessible if you have the Symbol getState(): EditorState { return this[MEMENTO_STATE]; }} interface EditorState { content: string; cursor: number; selection: { start: number; end: number } | null;} // The Originator uses the Symbol to access stateclass Editor { private content: string = ''; private cursor: number = 0; private selection: { start: number; end: number } | null = null; createMemento(): IMemento { const state: EditorState = { content: this.content, cursor: this.cursor, selection: this.selection ? { ...this.selection } : null }; return new Memento(state); } restore(memento: IMemento): void { // We know it's actually our Memento type // TypeScript requires type assertion here const m = memento as Memento; const state = m.getState(); this.content = state.content; this.cursor = state.cursor; this.selection = state.selection; }} // Caretaker only sees IMemento - the narrow interfaceclass HistoryManager { private mementos: IMemento[] = []; save(editor: Editor): void { const memento = editor.createMemento(); this.mementos.push(memento); // Cannot access memento.getState() - it's not on IMemento // Cannot access memento[MEMENTO_STATE] - Symbol is not exported // Can only use: memento.timestamp, memento.getDescription() }}The wide/narrow interface pattern is an application of Interface Segregation Principle (ISP). Different clients (Originator vs Caretaker) need different views of the Memento, so we provide exactly what each needs—no more, no less.
Designing an effective Memento requires careful consideration of several factors. The right choices depend on your specific use case.
Immutability is crucial:
Mementos must be immutable for correctness. If a memento could change after creation, restoration becomes unpredictable. Consider this problem:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// PROBLEM: Mutable state in mementoclass BadMemento { state: EditorState; // Mutable! constructor(state: EditorState) { this.state = state; // Shared reference! }} class ProblematicEditor { private state: EditorState = { content: '', cursor: 0 }; createMemento(): BadMemento { return new BadMemento(this.state); // Shares reference! } type(text: string): void { this.state.content += text; // This ALSO modifies the memento! }} // Usage that fails:const editor = new ProblematicEditor();const memento = editor.createMemento(); // Saves referenceeditor.type('Hello'); // Memento state is now also "Hello"!// Restoration is useless - memento has current state // SOLUTION: Always deep copy and make readonlyclass GoodMemento { private readonly state: Readonly<EditorState>; constructor(state: EditorState) { // Deep copy to break the reference this.state = Object.freeze({ content: state.content, cursor: state.cursor, // Deep copy nested objects too }); } getState(): EditorState { // Return a copy, not the original return { ...this.state }; }}When creating mementos, you must break all references to the originator's state. Use deep copying for nested objects, or you'll have subtle bugs where mutations to current state affect historical snapshots.
The classic Memento Pattern uses a dedicated Memento class, but there are several alternative approaches that might be more suitable depending on your context.
Approach: Serialize state to string/bytes instead of creating an object. Useful for persistence or network transfer.
1234567891011121314151617181920212223242526272829303132333435363738
class SerializableEditor { private content: string = ''; private cursor: number = 0; // Return serialized state as opaque string createMemento(): string { const state = { version: 1, // For future compatibility content: this.content, cursor: this.cursor, }; return JSON.stringify(state); } restore(memento: string): void { const state = JSON.parse(memento); if (state.version !== 1) { throw new Error('Incompatible memento version'); } this.content = state.content; this.cursor = state.cursor; }} // Caretaker stores stringsclass StringHistoryManager { private history: string[] = []; save(editor: SerializableEditor): void { // It's just a string - truly opaque! this.history.push(editor.createMemento()); } // Can persist to file, send over network, etc. saveToFile(path: string): void { writeFileSync(path, JSON.stringify(this.history)); }}We've explored the Memento Pattern's solution in depth—how it captures and restores state while preserving encapsulation. Let's consolidate the key concepts:
What's next:
Now that we understand the basic Memento structure, the next page dives deeper into the three participants—Originator, Memento, and Caretaker—examining their responsibilities, interactions, and design variations in detail.
You now understand how the Memento Pattern solves the state preservation problem through opaque snapshot objects that preserve encapsulation while enabling state capture and restoration.