Memento Pattern Lets You Undo Your Mistakes — Without Leaking Internals
In our Design Patterns series, we last looked at how the Builder pattern can clean up complex object construction in Builder Pattern Solves the Mess You Made with Constructors. Today, we’re shifting focus to another classic: the Memento pattern. This pattern is all about taking snapshots of an object’s state so you can roll back changes later — without exposing the object’s internals to the outside world.
What Is the Memento Pattern?
The Memento design pattern is a behavioral pattern that allows an object’s state to be saved and restored at a later time, without violating encapsulation. In simpler terms, the pattern lets you capture an object’s internal state (its member variables, etc.) in a separate memento object, so that you can revert the original object back to that state if needed. This is extremely handy for implementing undo/redo features (think of “undo” in an editor or a game) because you can revert to a previous state easily.
Alternate name: The Memento pattern is also known as the Snapshot pattern. This name hints at what’s happening — you take a snapshot of an object’s state to refer back to later.
The core purpose of Memento is to save and restore an object’s state without exposing the object’s implementation details. The object’s data remains encapsulated; only the object itself knows how to save or restore its state. External code (often called the “caretaker”) should not peek into or modify the saved state — it just holds onto it. One usage of Memento is to restore an object to its previous state (undo via rollback). It can also be used for things like state versioning or implementing custom object serialization for persistence.
Practically speaking, anytime you need a checkpoint mechanism — for example, the ability to revert a workflow or transaction if something goes wrong — Memento is a strong candidate. It’s commonly found in features like Undo/Redo functionality where each command or change can be undone by reverting to a saved state.
How the Memento Pattern Works
Let’s break down the mechanics of the Memento pattern. There are three key participants in this pattern:
- Originator: The object that has an internal state. It can create a memento containing a snapshot of its current state, and it can restore its state from a memento. The Originator knows which information to save and how to restore it.
- Memento: A separate object that stores the internal state of the Originator. The Memento’s contents are opaque to other objects — only the Originator should inspect or utilize the Memento. Think of it as a sealed snapshot or a “lock box” of state data.
- Caretaker: The object that orchestrates the saving and restoring process. The Caretaker decides when to ask the Originator for a memento (to save state) and when to restore a prior state. It holds onto one or more Mementos but does not modify their contents. The Caretaker might be a history manager, an undo manager, or just some client code that knows when to checkpoint and rollback. Importantly, the Caretaker should treat Mementos as opaque tokens — it shouldn’t try to interpret what’s inside.
Basic workflow: The typical interaction goes like this — The Caretaker asks the Originator for a snapshot (Memento) of its state before performing some operations. The Originator creates a Memento object representing its current state and hands it over. Later, if an “undo” or rollback is needed, the Caretaker passes that Memento back to the Originator, and the Originator uses it to restore its previous state. Throughout this process, the internals of the Originator’s state remain encapsulated; the Caretaker doesn’t know what it saved — just that it has a token to revert the Originator when needed. The caretaker first asks the originator for a memento… To roll back to the state before the operations, it returns the memento object to the originator. The memento object itself is an opaque object (which the caretaker cannot, or should not, change).
To illustrate, imagine a document editor (a classic scenario) where before making a big change, the editor app saves the current document state as a Memento. If the user hits “undo,” the app takes that Memento and restores the document content to the saved state. The editor doesn’t expose document internals to the undo manager; it simply provides a snapshot and can reapply it on command.
Encapsulation preserved: A big reason to use Memento is that it preserves encapsulation boundaries. The Originator is the only one who needs to know the details of its state. The Memento can be implemented in a way that no external code can see or alter the state it contains. For example, in many implementations, the Memento class is nested inside the Originator or has restricted access, so that others can only treat it as an opaque object. This ensures that external components (Caretakers or others) don’t accidentally (or maliciously) mess with the internals of the Originator — they simply hold the memento as a passive token.
Multiple snapshots (history): The pattern doesn’t limit you to a single checkpoint. A Caretaker can maintain a history list/stack of Mementos to enable multiple levels of undo or a timeline of state changes. For instance, an application could push a new Memento onto a stack each time the user performs an action, and pop from the stack to undo step by step. In this way, the Memento pattern can facilitate full undo/redo stacks or state history navigation (just be mindful of memory, which we’ll discuss later).
A note on scope: It’s important to note that the classic Memento pattern deals with saving the state of one object (the Originator) at a time. If your operation affects a whole bunch of objects that need to be consistent with each other, a single Memento might not capture the whole picture. The pattern, as usually implemented, “operates on a single object”. If an Originator’s state includes references to other objects or external resources, you need to be careful — changes in those won’t automatically be rolled back by restoring the Originator’s memento. In such cases, you might need multiple Mementos or a broader transaction mechanism.
Analogy: Checkpoints in a Video Game
To remember the concept, let’s use a real-world analogy that’s a bit more fun and unique than the usual text editor example. Imagine you’re playing an adventure video game that lets you save your progress at checkpoints. When you reach a safe point, you create a “save game” — this is essentially a snapshot of all your game state (your character’s inventory, health, position, level, etc.) at that moment. Now suppose you go fight a tough boss and things go horribly wrong. No worries — you can load the saved game from the checkpoint. The game restores all those saved variables (health, inventory, position…) back to the earlier state, effectively undoing all the damage (literally) that happened after the checkpoint.
In this analogy:
- The game itself (or the game engine) is the Originator — it has all the internal state (player stats, world state).
- The saved game file is the Memento — a snapshot of the game’s state at a point in time. This file is not meant to be edited by you; it’s a black box that only the game understands.
- You, the Caretaker, decide when to save (create a checkpoint memento) and when to load (restore from a memento). You don’t know the technical details of what’s in the save file — you just trust that the game will restore correctly from it.
This is precisely how many games implement save/load and undo-like mechanics — by serializing the game state to a snapshot. The concept is the same as the Memento pattern in software design. Just as a gamer relies on checkpoints to retry tricky sections without starting over, a program can use Mementos to revert an object to a safe state when something goes wrong.
Kotlin Example: Memento for Workflow State Management
Let’s build a concrete example in Kotlin to see the Memento pattern in action. Suppose we have a simple workflow with multiple steps (states) that an entity goes through — for example, an onboarding process or document approval flow. We want the ability to save the workflow’s state at certain checkpoints and roll back to a previous state if needed (like if an approval is rejected or an error occurs, we revert to an earlier step in the process).
In this example:
Workflow
will be our Originator. It has an internal state representing the current step of the workflow (and we can imagine it has other data too). It will provide methods to advance the workflow and to create/restore snapshots.WorkflowHistory
will act as the Caretaker, keeping a stack (history) of saved states (Mementos) and offering an undo operation.- A
Workflow.Snapshot
(nested class) is the Memento holding the state (here, the current step index).
We’ll simulate a workflow with three stages: Start, Processing, and Finish. We will advance through stages, save states, and undo to previous states. Here’s the full Kotlin code example:
// Originator
class Workflow(val name: String) {
private val steps = listOf("Start", "Processing", "Finish")
private var currentStep: Int = 0
fun proceed() {
if (currentStep < steps.lastIndex) {
currentStep++
}
// If already at last step, do nothing (can't proceed further).
}
fun createSnapshot(): Snapshot {
// Memento: capture the important state
return Snapshot(currentStep)
}
fun restoreSnapshot(snapshot: Snapshot) {
// Restore internal state from the snapshot
currentStep = snapshot.step
}
override fun toString(): String {
return "Workflow '$name' is at stage '${steps[currentStep]}'"
}
// Memento class capturing the state (inner class for encapsulation)
data class Snapshot(val step: Int)
}
// Caretaker
class WorkflowHistory {
private val history = mutableListOf<Workflow.Snapshot>()
fun backup(workflow: Workflow) {
history.add(workflow.createSnapshot())
}
fun undo(workflow: Workflow) {
if (history.isNotEmpty()) {
val snapshot = history.removeAt(history.lastIndex)
workflow.restoreSnapshot(snapshot)
}
}
}
// --- Usage example ---
fun main() {
val workflow = Workflow("Onboarding")
val history = WorkflowHistory()
println("Initial: $workflow") // Initial: Workflow 'Onboarding' is at stage 'Start'
history.backup(workflow) // Save initial state
workflow.proceed()
println("After first step: $workflow") // After first step: Workflow 'Onboarding' is at stage 'Processing'
history.backup(workflow) // Save state after first step
workflow.proceed()
println("After second step: $workflow") // After second step: Workflow 'Onboarding' is at stage 'Finish'
history.undo(workflow)
println("After undo 1: $workflow") // After undo 1: Workflow 'Onboarding' is at stage 'Processing'
history.undo(workflow)
println("After undo 2: $workflow") // After undo 2: Workflow 'Onboarding' is at stage 'Start'
}
In this code:
- The
Workflow.proceed()
method simulates moving the workflow to the next stage. We keep track of the current stage via a private index (currentStep
). We also overridetoString()
to make it easy to print the workflow’s status. - The
createSnapshot()
method inWorkflow
creates a newSnapshot
(memento) containing the current step index. Notice thatSnapshot
is defined insideWorkflow
– this hints that it’s tightly coupled toWorkflow
and not meant for general use elsewhere. In a more encapsulated design,Snapshot
might even be a private inner class or implement an interface so that outside code cannot read its contents. - The
restoreSnapshot()
method takes aSnapshot
and sets thecurrentStep
back to the saved value, effectively rolling back the workflow to that stage. WorkflowHistory
is a simple caretaker that uses a list to store snapshots. Itsbackup()
method asks the originator (Workflow
) for a snapshot of its current state and stores it. Theundo()
method pops the last saved snapshot from history and asks the workflow to restore itself. Notice howWorkflowHistory
doesn’t know what it’s saving – it just holdsWorkflow.Snapshot
objects. It treats them as opaque snapshots.- In
main()
, we simulate the workflow progressing through two steps (from Start to Processing to Finish), with backups taken at the initial state and after the first step. We then perform two undos: the first undo brings the workflow from Finish back to Processing, and the second undo brings it from Processing back to Start. The printed output (as shown in the comments) confirms that the state was restored correctly each time.
This example is deliberately simplified (we only saved the step index). In a real workflow, the Snapshot
might contain a lot more data – perhaps form inputs, status flags, timestamps, etc. The concept remains the same: capture whatever state is needed to fully restore the workflow later. The Originator (Workflow) decides what to include in the snapshot and how to use it to restore. The Caretaker (WorkflowHistory) just triggers save/restore at the right times and holds the snapshots in between.
A key thing to observe is that we didn’t expose any of Workflow
’s internal fields to the outside. The Workflow
class encapsulates the state and provides controlled methods for saving and restoring. External code isn’t reaching in to grab currentStep
or modify it directly; it has to go through createSnapshot
and restoreSnapshot
. This ensures that the internal representation can change without breaking external code – the hallmark of encapsulation.
Cons and Limitations
Let’s skip the sales pitch — by now, the benefits of the Memento pattern should be clear: it lets you preserve and restore object state without breaking encapsulation, it’s great for undo/redo systems, and it keeps business logic cleanly separated from control flow. But the story doesn’t end there. As with any pattern, using it introduces trade-offs worth understanding:
- Memory overhead: Each saved state is an additional object consuming memory. If the Originator’s state is large or if you create mementos frequently, you can quickly use a lot of memory. For example, an image editor storing a memento of an entire high-resolution image after every edit would consume enormous memory. This overhead means you should be mindful about how many snapshots you keep and how big they are. In practice, one often limits the number of undo levels or uses strategies like differential snapshots (storing only changes) to mitigate this.
- Performance cost: Creating a memento might involve copying the entire state of an object. If done often, this can impact performance, especially for large state or in performance-critical applications. Restoring state also might have costs (e.g. reinitializing complex structures). Essentially, you are trading CPU/memory to gain the convenience of easy rollback. If the frequency of state changes is high, a naive memento approach (snapshot on every tiny change) can be inefficient. Caretakers might need to implement throttling or debouncing strategies (e.g., don’t capture new mementos too often or if state hasn’t changed much).
- Doesn’t automatically manage external resources: A memento typically covers one object’s state. If the Originator’s state is interdependent with other objects or system resources, restoring one object’s state might not revert the others. The classic Memento pattern “operates on a single object” and doesn’t inherently solve multi-object consistency. For instance, if your workflow object’s state depends on two different subsystems, you might need a broader mechanism to capture both. Without careful design, you could restore one part of state and leave another part unchanged, leading to inconsistency. This is a known limitation — addressing it might require multiple mementos or a custom composite memento that aggregates state from multiple objects (which adds complexity).
- Complexity for very large state: If an object has a very complex state (lots of fields, collections, deep object graphs), writing the code to snapshot and restore it can be error-prone. You need to ensure every relevant piece of state is saved and correctly restored. Missing something could mean the object isn’t fully reverted. This can make the implementation of Memento for large classes somewhat complex and tedious. There’s also the question of mutable vs immutable state — if your state includes references to mutable objects or collections, you have to decide whether to deep copy them for the memento (to truly capture that moment in time) which can be expensive, or to risk them changing underneath you (not advisable). All that adds design complexity.
- Overuse can be overkill: In scenarios where a simple solution exists (like just keeping a single backup value for one or two variables), implementing Memento might be overengineering. For example, to implement a basic “undo” for a text field, you might just keep the last string value rather than a whole Memento system. It’s important to gauge the complexity of the problem — sometimes a full Memento-based history stack is more than you need.
In summary, Memento shines for undo/rollback situations and when you need to preserve encapsulation, but be cautious about the memory and complexity implications. Next, we’ll discuss how to determine if Memento is the right tool for your situation.
When (Not) to Use the Memento Pattern
The Memento pattern excels when you need to support undo, rollback, or versioning without compromising object encapsulation. If your application requires reverting objects to previous states — for instance, in an editor, game, or approval flow — Memento gives you a clean, encapsulated mechanism to do just that. It’s particularly useful when you want to save state without exposing or coupling to an object’s internals. It also fits naturally into any context where checkpoints, snapshots, or scenario switching are relevant. As long as the state is relatively lightweight or changes aren’t constant, the memory and performance trade-offs remain reasonable.
However, there are caveats. If you’re dealing with massive, rapidly changing state, the cost of repeatedly saving full snapshots can be prohibitive. In such cases, alternatives like storing only diffs, event sourcing, or command-based undo are often more efficient. Also, Memento isn’t great when you need to coordinate changes across multiple interdependent objects — its scope is typically limited to a single originator. Lastly, if you’re only saving a couple of values or need a one-step rollback, a simpler mechanism might be enough. Memento shines in structured undo systems, not for trivial history storage.
Strengths, Pitfalls, and Edge Cases
Memento’s key strength is its respect for encapsulation: it allows an object to save and restore its state without exposing its internals to outside interference. This makes it ideal for clean undo/redo implementations and state history management. You gain a lot of flexibility — the originator owns its state-saving logic, and caretakers can trigger restores without any insight into what’s inside the snapshot.
But the simplicity of usage belies certain risks. Snapshotting complex or mutable state can introduce subtle bugs if not handled properly. Memory usage can balloon if you store too many full snapshots or forget to prune them over time. And unlike more granular strategies, Memento requires you to store full state, not just changes — which is both a blessing and a burden.
Finally, restoring a state doesn’t necessarily revert side effects or interactions with other objects, which can lead to inconsistencies. You’ll need to assess whether the snapshot alone is sufficient, or whether a broader coordination mechanism is necessary. When done thoughtfully, though, Memento gives you a robust and elegant way to time-travel within your code without breaching object boundaries.
Conclusion
The Memento pattern offers a way to restore past states without compromising object integrity or exposing internal guts. By capturing and externalizing an object’s state, you can perform magical feats like time-traveling backward (and forward) in an object’s life cycle. We saw how it enables undo/redo functionality, lets us rollback on errors, and helps implement features like state history or versioning — all without violating encapsulation or object integrity. As with any magic, it comes at a cost: there’s no free resources when it comes to memory usage, and you have to manage those snapshots wisely (too many mementos or very large ones can bite you). Nonetheless, when used appropriately, Memento provides an elegant way to handle state that changes over time.
Have you ever designed an undo system or a rollback mechanism in your projects? Did you use the Memento pattern, or do you prefer other methods like commands or diffing? Share your experiences or thoughts in the comments — it’s always fascinating to hear how others tackle these problems.
If you found this article insightful, give it a clap (or several), share it with your fellow devs, and don’t forget to subscribe for more design pattern deep-dives! Happy coding, and may your past state snapshots always restore correctly!