Memento Design Pattern | UML + Code | System Design
The Memento Design Pattern is a specialized, highly practical behavioral design pattern used to capture, externalize, and restore an object's internal state to a previous snapshot without breaking encapsulation. It is the architectural cornerstone behind ubiquitous features like database rollbacks, undo/redo mechanisms in text editors, and version control systems like Git.
1. Why Do We Need the Memento Pattern? (The Problem Statement)
In complex software systems, objects frequently undergo state mutations during execution. Whether processing financial records, executing database transactions, or updating a game character's attributes, objects evolve through multiple states.
However, real-world operations often fail midway due to network partitions, invalid user inputs, or unexpected exceptions, leaving objects in corrupted, half-mutated, or inconsistent states.
The Status Quo vs. The Problem
- The Naive Approach: To implement undo functionality, developers often expose internal object fields via public getters and setters (
getters/setters), letting external client code manually copy and store raw values. - The Encapsulation Violation: Exposing internal representations breaks encapsulation (a core OOP pillar). If an object's internal data structures change (e.g., refactoring a
Mapinto a custom relational model), all external client code tracking those states breaks instantly. - The Solution: We need a clean architectural mechanism that takes a verified point-in-time snapshot (or screenshot) of an object's internal state, stores it securely outside the object, and allows seamless rollbacks when failures occur—all without exposing the object's internal implementation details.
2. Core Mental Model: Real-Life Analogies
To master the Memento pattern, visualize how everyday systems manage history and recovery:
Real-Life Analogy 1: Database Transactions & The "Savepoint"
Imagine editing a spreadsheet or a banking ledger where you execute 5 sequential updates. If update #4 throws a network timeout error, you don't want the ledger left half-updated (e.g., balance deducted from Account A, but never credited to Account B). Instead, you issue a Rollback to the exact stable snapshot captured before the transaction began.
Real-Life Analogy 2: Video Game Checkpoints
In an RPG video game, your character (Originator) traverses a dangerous dungeon, changing health, inventory, and experience points. Before entering a boss fight, you save your game at a checkpoint (Memento). If your character dies, the system reloads the checkpoint, instantly restoring your exact previous health and inventory without you needing to replay the entire game manually.
3. The Three Pillars of the Memento Pattern
The Memento design pattern relies strictly on three distinct actors, each with a single, well-defined responsibility:
4. Naive State Management vs. Memento Architecture
To appreciate why the Memento pattern is robust, contrast a coupled naive implementation with the decoupled Memento architecture.
Naive / Coupled Approach (Anti-Pattern)
// Anti-Pattern: Exposing internals to client code
public class Database {
public Map<String, String> records = new HashMap<>(); // Public/Accessible internals
}
// Client manually extracts and stores raw state
Map<String, String> backup = new HashMap<>(db.records);
// If Database internals change from Map to Tree, client code breaks!
Clean Memento Architecture
// Client interacts exclusively through well-defined boundaries
TransactionManager (CareTaker)
|
+-- requests snapshot from --> Database (Originator)
| |
| v
| DatabaseMemento (Snapshot Holder)
|
+-- holds backup reference safely without inspecting contents
5. Step-by-Step System Evolution: Building a Database Transaction Manager
Let's design a robust Database Transaction Manager in Java that demonstrates the Memento pattern in action, complete with beginTransaction, commit, and rollback semantics.
Step 5.1: The Originator (Database)
The Database class holds an in-memory Map<String, String> representing records. It provides CRUD operations alongside two crucial methods: createMemento() and restoreFromMemento().
import java.util.HashMap;
import java.util.Map;
public class Database {
private Map<String, String> records;
public Database() {
this.records = new HashMap<>();
}
public void insert(String key, String value) {
records.put(key, value);
}
public void update(String key, String value) {
records.put(key, value);
}
public void remove(String key) {
records.remove(key);
}
// Creates a snapshot (Memento) of the current state
public DatabaseMemento createMemento() {
return new DatabaseMemento(this.records);
}
// Restores internal state from a given Memento
public void restoreFromMemento(DatabaseMemento memento) {
this.records = memento.getState();
}
public void displayRecords() {
System.out.println("Current Database Records: " + records);
}
}
Step 5.2: The Memento (DatabaseMemento)
The Memento is immutable or restricted, holding the raw snapshot data (Map). It encapsulates the state so external classes cannot tamper with it.
import java.util.HashMap;
import java.util.Map;
public class DatabaseMemento {
private final Map<String, String> data;
// Constructor captures a deep copy of the records map
public DatabaseMemento(Map<String, String> dbData) {
this.data = new HashMap<>(dbData);
}
// Package-private or restricted getter for the Originator's eyes only
public Map<String, String> getState() {
return new HashMap<>(this.data);
}
}
Step 5.3: The Caretaker (TransactionManager)
The TransactionManager oversees transaction lifecycles. It holds a reference to the DatabaseMemento backup, handling beginTransaction, commit, and rollback workflows.
public class TransactionManager {
private DatabaseMemento backup;
public TransactionManager() {
this.backup = null;
}
public void beginTransaction(Database db) {
// Clear any old stale backups
if (this.backup != null) {
this.backup = null;
}
// Take snapshot before mutating state
this.backup = db.createMemento();
System.out.println("[Transaction] Transaction began. Snapshot created.");
}
public void commitTransaction() {
// Everything succeeded; discard backup
this.backup = null;
System.out.println("[Transaction] Transaction committed successfully. Backup cleared.");
}
public void rollbackTransaction(Database db) {
if (this.backup != null) {
// Restore previous stable state
db.restoreFromMemento(this.backup);
this.backup = null;
System.out.println("[Transaction] Error occurred! Rolled back to previous stable state.");
} else {
System.out.println("[Transaction] No active backup available for rollback.");
}
}
}
6. Complete Flow Walkthrough (Simulation & Execution)
Let's test our complete system by simulating a successful transaction followed by a failed transaction requiring a rollback.
Client Test Driver (Main.java)
public class Main {
public static void main(String[] args) {
Database db = new Database();
TransactionManager txManager = new TransactionManager();
// --- SCENARIO 1: Successful Transaction & Commit ---
txManager.beginTransaction(db);
db.insert("User1", "Aditya");
db.insert("User2", "Rohit");
db.displayRecords(); // Output: {User1=Aditya, User2=Rohit}
txManager.commitTransaction();
System.out.println("--------------------------------------------------");
// --- SCENARIO 2: Failed Transaction & Rollback ---
txManager.beginTransaction(db); // Snapshot taken with {User1=Aditya, User2=Rohit}
db.insert("User3", "Sourav");
db.insert("User4", "Manish");
db.displayRecords(); // Output includes User3 and User4
// Simulate a runtime exception / failure
boolean errorOccurred = true;
if (errorOccurred) {
txManager.rollbackTransaction(db);
} else {
txManager.commitTransaction();
}
// Final Verification
db.displayRecords(); // Output reverts to: {User1=Aditya, User2=Rohit}
}
}
Execution Flow Diagram
7. Real-World Practical Pitfalls & Edge Cases
When implementing the Memento pattern in production enterprise systems, engineers must account for several architectural trade-offs:
- High Memory Consumption: If an Originator is massive (e.g., gigabytes of cached data) and state changes occur thousands of times per second, keeping a list or chain of heavy Mementos can easily trigger
OutOfMemoryError.- Mitigation: Store only incremental diffs (deltas) instead of full deep copies, or limit history size using a bounded FIFO queue.
- Garbage Collection Pressure: Creating frequent object snapshots increases GC overhead in languages like Java or C#.
- Mitigation: Object pooling or reusing Memento instances where applicable.
- Encapsulation Leaks: If a Memento exposes internal collection references directly without wrapping them in defensive copies (
new HashMap<>(original)), external callers can accidentally mutate historical state.
8. Standard UML Class Structure
Summary and Key Takeaways
- Preserves Encapsulation: The Memento pattern allows capturing and restoring an object's internal state without exposing its private fields to the outside world.
- Core Participants: Consists of three clear actors: Originator (the state owner), Memento (the immutable snapshot container), and CareTaker (the history manager).
- Separation of Concerns: The CareTaker holds and manages mementos without needing to know how the internal state is structured or represented.
- Database Rollbacks: Ideal for implementing ACID transaction semantics, database savepoints, and automatic error recovery workflows.
- Undo / Redo Capabilities: Powers interactive application features such as text editor history, graphic design tool undo trees, and game checkpoints.
- Git & Version Control: Conceptually mirrors Git commits and branch checkouts, where snapshots of codebase states are stored and retrieved upon demand.
- Memory Management Warning: Be mindful of memory overhead when dealing with large state objects; implement bounded history limits or incremental diffing to prevent memory bloat.