Loading content...
Consider a familiar scenario: you're working in a text editor, making significant changes to a document. You delete a paragraph, reformat a section, then realize you've made a mistake. You press Ctrl+Z and—like magic—the document reverts to its previous state. Press it again, and you go back even further.
This seemingly simple feature conceals a profound architectural challenge. The editor must somehow:
The last requirement is the critical constraint. We want to capture state without knowing how that state is structured internally. This is the state preservation dilemma—and it's the problem the Memento Pattern elegantly solves.
By the end of this page, you will understand: • Why capturing object state is architecturally challenging • How naive approaches violate encapsulation • The tension between state access and information hiding • Real-world scenarios where state preservation is essential • The specific requirements that lead to the Memento Pattern
At the heart of object-oriented design lies encapsulation—the principle that an object's internal state should be hidden from the outside world. External code interacts with objects through well-defined interfaces, never directly manipulating internal data.
Encapsulation provides crucial benefits:
But now consider state preservation. To save an object's state, we need access to that state. To restore it, we need the ability to set it. This appears to directly contradict encapsulation.
Encapsulation says: Hide internal state, expose only behavior. State preservation says: Access internal state to save and restore it.
These requirements seem mutually exclusive. The naive solution—exposing state through getters and setters—defeats the purpose of encapsulation entirely.
Let's examine why this tension is so problematic by considering a concrete example.
123456789101112131415161718192021222324252627282930313233343536373839404142
// A text editor with rich internal stateclass TextEditor { // These should be private - internal implementation details private content: string; private cursorPosition: number; private selectionStart: number | null; private selectionEnd: number | null; private scrollPosition: number; private undoStack: string[]; private redoStack: string[]; private formatting: Map<number, TextFormat>; private bookmarks: Set<number>; // The problem: How do we capture ALL of this state // for undo/redo without exposing these internals? // Naive approach: Expose everything public getContent(): string { return this.content; } public getCursorPosition(): number { return this.cursorPosition; } public getSelectionStart(): number | null { return this.selectionStart; } public getSelectionEnd(): number | null { return this.selectionEnd; } public getScrollPosition(): number { return this.scrollPosition; } public getFormatting(): Map<number, TextFormat> { return this.formatting; } public getBookmarks(): Set<number> { return this.bookmarks; } // And setters for restoration... public setContent(content: string): void { this.content = content; } public setCursorPosition(pos: number): void { this.cursorPosition = pos; } // ... many more setters // This approach has serious problems: // 1. We've exposed all internal structure // 2. Any code can now manipulate state directly // 3. Changing internal representation breaks all callers // 4. We've lost invariant protection} interface TextFormat { bold: boolean; italic: boolean; fontSize: number;}This naive approach transforms a well-encapsulated class into an open data container. Every piece of internal state becomes accessible, and worse, modifiable by any external code. We've solved the state preservation problem by destroying the architecture.
State preservation isn't just an academic concern—it's a requirement in numerous real-world applications. Understanding these scenarios helps us appreciate why we need a principled solution.
| Domain | Use Case | State Complexity | Challenge |
|---|---|---|---|
| Text Editors | Undo/Redo operations | Document content, formatting, cursor, selection | Potentially large state, frequent snapshots |
| Graphics Software | Layer history, filter reversal | Pixel data, layer hierarchy, blend modes | Very large state (megabytes per snapshot) |
| Games | Save/Load, checkpoints | Player state, world state, inventory, quests | Interconnected object graphs |
| Database Transactions | Rollback support | Row data, index state, lock state | Consistency across multiple objects |
| Form Wizards | Back navigation | User inputs, validation state, step history | Partial state across multiple screens |
| IDEs | Refactoring undo | AST, file content, references, imports | Cascading changes across files |
| Financial Systems | Transaction reversal | Account balances, audit trail, timestamps | Regulatory requirements for state history |
| CAD Software | Design versioning | Geometric models, constraints, materials | Complex object relationships |
Each of these scenarios shares common characteristics:
Let's examine one scenario in depth to understand the full complexity.
12345678910111213141516171819202122232425262728293031323334
// Consider a graphics editor layer - rich internal stateclass GraphicsLayer { private id: string; private name: string; private visible: boolean; private locked: boolean; private opacity: number; private blendMode: BlendMode; private position: { x: number; y: number }; private rotation: number; private scale: { x: number; y: number }; private pixels: Uint8ClampedArray; // Could be megabytes! private mask: Uint8ClampedArray | null; private effects: LayerEffect[]; private parent: GraphicsLayer | null; private children: GraphicsLayer[]; // The state preservation challenge: // 1. pixels array could be 4MB for a 1000x1000 image // 2. effects have their own internal state // 3. parent/children create object graph complexity // 4. Some state is cheap to copy, some is expensive // We need selective, efficient state capture // But we don't want callers to know these details} type BlendMode = 'normal' | 'multiply' | 'screen' | 'overlay'; interface LayerEffect { type: string; parameters: Map<string, unknown>; enabled: boolean;}In graphics applications, state ranges from trivial (a boolean flag) to massive (pixel arrays). An effective state preservation mechanism must handle both efficiently without exposing the distinction to external code.
Before arriving at the Memento Pattern, developers often try several approaches that seem reasonable but ultimately fail. Understanding why they fail illuminates the need for a better solution.
1234567891011121314151617181920212223242526272829303132333435
// ANTI-PATTERN: Exposing state through accessorsclass Document { private paragraphs: Paragraph[]; private styles: StyleSheet; private metadata: DocMetadata; // "For state preservation" getParagraphs(): Paragraph[] { return this.paragraphs; } setParagraphs(p: Paragraph[]): void { this.paragraphs = p; } getStyles(): StyleSheet { return this.styles; } setStyles(s: StyleSheet): void { this.styles = s; } // ... // Problems: // 1. Anyone can call document.setParagraphs([]) // 2. No validation on restoration // 3. If we change from Paragraph[] to LinkedList, all callers break} // The "undo manager" now knows everything about Document internalsclass BadUndoManager { save(doc: Document): SavedState { // We're deeply coupled to Document's internal structure return { paragraphs: doc.getParagraphs(), styles: doc.getStyles(), // Must update this whenever Document changes! }; } restore(doc: Document, state: SavedState): void { doc.setParagraphs(state.paragraphs); doc.setStyles(state.styles); }}123456789101112131415161718192021222324252627
// ANTI-PATTERN: Cloning entire objectsclass GameCharacter { private health: number; private inventory: Item[]; private position: Vector3; private currentQuest: Quest; private allies: GameCharacter[]; // Reference to other characters! clone(): GameCharacter { const copy = new GameCharacter(); copy.health = this.health; copy.inventory = [...this.inventory]; // Shallow copy - items shared! copy.position = { ...this.position }; copy.currentQuest = this.currentQuest; // Same reference! copy.allies = [...this.allies]; // Shallow copy - characters shared! return copy; }} // Problems with clone approach:// 1. What happens when we restore a clone?// - The clone IS a different object// - References to the original are now stale// - We'd need to swap object identity somehow// 2. Shared mutable state (inventory items, allies) causes chaos// 3. Clone method must know all internal fields// 4. Very expensive for large objects123456789101112131415161718192021222324252627
// ANTI-PATTERN: JSON serialization for stateclass FormState { private data: Map<string, unknown>; // Map doesn't JSON.stringify! private validators: ((v: unknown) => boolean)[]; // Functions! private submitDate: Date; // Becomes a string! private processedBy: Set<string>; // Set doesn't JSON.stringify! serialize(): string { // This won't work correctly! return JSON.stringify({ data: this.data, // {} - Map is empty object validators: this.validators, // undefined - functions lost submitDate: this.submitDate, // string, not Date processedBy: this.processedBy, // {} - Set is empty object }); } deserialize(json: string): void { const parsed = JSON.parse(json); // Now we have to manually reconstruct everything this.data = new Map(Object.entries(parsed.data)); this.validators = ???; // They're gone forever this.submitDate = new Date(parsed.submitDate); this.processedBy = new Set(parsed.processedBy); // This is fragile, error-prone, and exposes structure }}Having seen why naive approaches fail, we can now articulate the precise requirements for a proper state preservation mechanism. A solution must satisfy all of these constraints simultaneously.
Here's the key insight: we need someone to have access to internal state for capture and restoration, but that someone shouldn't be external code. The object itself must control state access. This leads directly to the Memento Pattern's design.
The requirements can be summarized in a single architectural constraint:
The object must expose its state to itself at a different point in time, without exposing that state to anyone else.
This seemingly paradoxical requirement is exactly what the Memento Pattern achieves through its clever use of access control and intermediate objects.
Not all objects require sophisticated state preservation. Understanding when the problem becomes non-trivial helps us recognize when the Memento Pattern is truly needed.
State types that complicate preservation:
Computed/Derived State: Some state is derived from other state. Should we capture it or recompute on restore?
Transient State: Some state shouldn't be persisted (open file handles, active network connections).
Shared References: Objects pointing to shared data raise questions: capture the reference or the data?
Large State: When state is megabytes (images, documents), naive copying is prohibitive.
Validated State: If there are invariants between fields, snapshots must maintain them.
12345678910111213141516171819202122232425262728293031
// Example: A complex object with various state typesclass SpreadsheetCell { // Core state - must be preserved private formula: string; private rawValue: CellValue; // Derived state - can be recomputed private displayValue: string; // Formatted version of rawValue private computedResult: number; // Result of formula evaluation // Transient state - should NOT be preserved private isBeingEdited: boolean; private editBuffer: string; private highlightTimer: ReturnType<typeof setTimeout> | null; // Reference state - complex decision private stylesheet: Stylesheet; // Shared with many cells private dependencies: SpreadsheetCell[]; // References to other cells private dependents: SpreadsheetCell[]; // Cells that reference us // The preservation challenge: // - Formula and rawValue: definitely save // - displayValue: can recompute, but expensive for many cells // - isBeingEdited: definitely don't save // - dependencies: save references, not cell copies // - stylesheet: save reference only // Only the cell itself knows these distinctions!} type CellValue = string | number | boolean | Date | null;Having thoroughly examined the problem, we can now articulate the insight that leads to the Memento Pattern.
The key realization is this: We don't need to expose state to external code. We need to create a state container that:
This container—the Memento—is opaque to everything except the object that created it. External systems (undo managers, history trackers) handle mementos like sealed envelopes: they can store them, order them, and pass them back, but they can never read what's inside.
The Memento Pattern separates state management from state storage. The object manages what state to capture and how to restore it. External code manages when to capture, how long to keep, and when to restore. Neither needs to know the other's business.
We've thoroughly examined the state preservation problem—a fundamental challenge in object-oriented design. Let's consolidate what we've learned:
What's next:
Now that we understand the problem in depth, we're ready to explore the solution. The next page introduces the Memento Pattern's structure—the Originator, Memento, and Caretaker roles that together solve the state preservation challenge while preserving encapsulation.
You now understand the state preservation problem: why it's architecturally challenging, why naive solutions fail, and what requirements a proper solution must meet. Next, we'll see how the Memento Pattern elegantly addresses all of these constraints.