Java design pattern deep dive
Memento Pattern in Java: 100 interview questions with professional answers.
Learn how the Memento pattern captures and restores an object's internal state without breaking encapsulation, how to build safe, bounded undo/redo stacks in Java, and how the same idea scales from a text editor's undo history all the way up to event-sourced systems.
What makes a good Memento answer?
Interviewers want to see that you understand the encapsulation guarantee at the heart of this pattern, not just "an undo stack": the Caretaker's job is storage and lifecycle only, and the memento's contents stay private to the Originator that created them.
| Approach | Use when | Watch out for |
|---|---|---|
| In-memory Memento object | Undo/redo within a single running process, such as a desktop or web-session editor, where restore latency must be near-instant. | Unbounded history growth can exhaust heap; bound the stack and consider diffs for large snapshots. |
| Java serialization snapshot | You need to persist a memento to disk, ship it across a process boundary, or version a save file format. | Slower than plain object copying, fragile to class-shape changes, and needs careful serialVersionUID management. |
| Database savepoint / transaction rollback | The state to protect already lives in a relational database and the "undo" is really "abort this unit of work". | Only covers what the database manages; in-memory object state and side effects like emails already sent are not rolled back. |
| Event-sourcing rebuild-from-events | You need a full audit trail and the ability to reconstruct state at any point in history, not just the last few steps. | Replaying a long event log is slow without periodic snapshots, which are themselves just mementos taken at scale. |
Topics
Interview questions and answers
Each answer gives the implementation direction, the trade-off to mention, and the production concern that makes the answer stronger.
1. Explain the Memento design pattern in Java and describe the real-world problem it solves when you need to support undoing a change to an object's state.
The Memento pattern captures an object's internal state at a point in time, outside of that object, so it can be restored later, without exposing the object's internal representation to whatever code is holding onto that captured snapshot. It solves the problem of adding undo, rollback, or checkpoint behavior to an object without breaking encapsulation by making its private fields public just so some external "history manager" can read and rewrite them.
Three roles cooperate: the Originator, whose state is captured; the Memento, an opaque snapshot object; and the Caretaker, which stores mementos but never looks inside them.
class TextDocument {
private String content = "";
Memento save() { return new Memento(content); }
void restore(Memento m) { this.content = m.getContent(); }
static class Memento {
private final String content;
private Memento(String content) { this.content = content; }
private String getContent() { return content; }
}
}
2. Describe the three participants in the Memento pattern, Originator, Memento, and Caretaker, and explain the exact responsibility boundary between them.
The Originator is the object whose state matters; it knows how to build a Memento from its own current state and how to restore its state from a Memento it is given back. The Memento is a value object that holds a snapshot of the Originator's state; it exposes little or nothing publicly. The Caretaker requests mementos from the Originator, stores them, for example in a stack, and later hands one back to the Originator when a rollback is requested, but it never reads or modifies what is inside.
The boundary is strict on purpose: only the Originator understands the shape of its own state, so only the Originator is trusted to create and interpret mementos.
3. Why is preventing the Caretaker from reading a memento's internal contents considered the whole point of the Memento pattern, rather than an incidental detail?
If the Caretaker could read or modify a memento's internals, you would not need the pattern at all; you could just expose the Originator's fields directly and let any code build its own "snapshot" struct. The entire value of Memento is that undo/history infrastructure can be written once, generically, and reused for any Originator, without that infrastructure needing to know anything about what state each Originator actually holds.
Break that boundary and you also break the Originator's ability to change its internal representation later, since now some external Caretaker code depends on the old shape of that state.
4. Explain the "narrow interface, wide interface" technique from the original Gang of Four description of Memento, and show how it is implemented in Java.
The Memento exposes two interfaces: a narrow one, with no visible methods at all, seen by the Caretaker, and a wide one, with full read access, seen only by the Originator that created it. In Java this is usually done by making the Memento a private static nested class of the Originator: the outer world only ever sees an opaque reference type, while the Originator's own code has full access to the nested class's private fields because nested classes share access with their enclosing class.
class Editor {
interface Memento {} // narrow interface: nothing exposed
private static class EditorMemento implements Memento {
private final String text;
private EditorMemento(String text) { this.text = text; }
}
private String text = "";
Memento save() { return new EditorMemento(text); } // wide access here
void restore(Memento m) { this.text = ((EditorMemento) m).text; }
}
5. Walk through implementing a Memento as a private static nested class inside the Originator, and explain why static (rather than inner, non-static) is usually the right choice.
Making the Memento class private means it cannot be referenced by name outside the Originator's own source file, so the Caretaker can only hold it through an opaque marker type. Making it static, rather than a non-static inner class, avoids an implicit reference back to the enclosing Originator instance, which matters because a non-static inner class instance keeps its outer instance alive and reachable, which can be a subtle memory leak if mementos are kept around long after the Originator itself should be collectable.
class Game {
interface Save {}
private static final class GameSave implements Save {
private final int level;
private final int score;
private GameSave(int level, int score) { this.level = level; this.score = score; }
}
private int level;
private int score;
Save save() { return new GameSave(level, score); }
void load(Save s) {
GameSave gs = (GameSave) s;
this.level = gs.level;
this.score = gs.score;
}
}
6. Walk through a complete worked example of undo/redo in a simple text editor using the Memento pattern, identifying the Originator, Memento, and Caretaker concretely.
The Originator is the document, holding text and a cursor position. The Memento captures both fields as an immutable snapshot. The Caretaker is an undo stack that the editor's controller pushes to before every edit and pops from when the user presses Ctrl+Z.
class Document {
private String text = "";
private int cursor = 0;
Memento snapshot() { return new Memento(text, cursor); }
void restore(Memento m) { this.text = m.text; this.cursor = m.cursor; }
void type(String s) { text = text.substring(0, cursor) + s + text.substring(cursor); cursor += s.length(); }
static final class Memento {
private final String text;
private final int cursor;
private Memento(String text, int cursor) { this.text = text; this.cursor = cursor; }
}
}
class UndoStack {
private final java.util.Deque<Document.Memento> history = new java.util.ArrayDeque<>();
void record(Document doc) { history.push(doc.snapshot()); }
void undo(Document doc) { if (!history.isEmpty()) doc.restore(history.pop()); }
}
The controller calls undoStack.record(doc) just before mutating doc, so each undo restores the state as it was immediately prior to that edit.
7. When designing a Memento, how do you decide exactly which fields of the Originator's state should be captured, a full deep snapshot versus a minimal, targeted subset?
Capture exactly the fields that undo needs to restore, no more and no less: including extra fields wastes memory and risks capturing something that later changes shape and breaks old snapshots, while omitting a field means a "successful" undo silently leaves stale state behind. For a text editor that might be the full text plus cursor position; for a form wizard it might be only the current step's inputs, not the whole application's state.
A useful test is to ask, "if I restore this memento and then compare the Originator's public behavior before and after, is there any observable difference?" If yes, something is missing from the snapshot.
8. How would you implement a bounded undo history in Java, a Caretaker that keeps only the most recent N mementos and evicts the oldest one once the limit is reached?
A Deque used as a stack works well: push new mementos onto one end, and when the size exceeds the limit, remove from the opposite end (the oldest entry) rather than growing forever. This keeps memory bounded regardless of how long an editing session runs.
class BoundedUndoStack {
private final int maxSize;
private final java.util.Deque<Document.Memento> stack = new java.util.ArrayDeque<>();
BoundedUndoStack(int maxSize) { this.maxSize = maxSize; }
void push(Document.Memento m) {
stack.addFirst(m);
if (stack.size() > maxSize) {
stack.removeLast(); // evict oldest
}
}
Document.Memento pop() { return stack.isEmpty() ? null : stack.removeFirst(); }
}
9. Explain how a redo stack works alongside an undo stack in a Memento-based editor, and why performing a brand-new edit after an undo must clear the redo stack.
Undo pops a memento off the undo stack, restores it, and pushes the state the document was in just before that restore onto the redo stack, so redo can move forward again. But if the user then types something new rather than pressing redo, the redo stack's saved states describe a future that no longer exists; continuing to allow redo after a fresh edit would silently jump the document to a state that has nothing to do with what the user just typed.
class UndoRedoManager {
private final java.util.Deque<Document.Memento> undoStack = new java.util.ArrayDeque<>();
private final java.util.Deque<Document.Memento> redoStack = new java.util.ArrayDeque<>();
void recordBeforeEdit(Document doc) {
undoStack.push(doc.snapshot());
redoStack.clear(); // new edit invalidates the redo history
}
void undo(Document doc) {
if (undoStack.isEmpty()) return;
redoStack.push(doc.snapshot());
doc.restore(undoStack.pop());
}
void redo(Document doc) {
if (redoStack.isEmpty()) return;
undoStack.push(doc.snapshot());
doc.restore(redoStack.pop());
}
}
10. Show how the Memento pattern combines with the Command pattern to build a full undo/redo system, where each Command object stores the memento it needs to undo itself.
Rather than the Caretaker managing raw mementos directly, each executed Command captures a Memento of the Originator's state immediately before it runs, and its undo() method simply restores that stored memento. The Caretaker then only needs to keep a stack of Command objects, which gives you both "what changed" (useful for logging or a visible history list) and "how to undo it" in one object.
interface Command {
void execute();
void undo();
}
class TypeTextCommand implements Command {
private final Document doc;
private final String textToInsert;
private Document.Memento before;
TypeTextCommand(Document doc, String textToInsert) {
this.doc = doc;
this.textToInsert = textToInsert;
}
@Override public void execute() {
before = doc.snapshot();
doc.type(textToInsert);
}
@Override public void undo() { doc.restore(before); }
}
class CommandHistory {
private final java.util.Deque<Command> executed = new java.util.ArrayDeque<>();
void run(Command c) { c.execute(); executed.push(c); }
void undoLast() { if (!executed.isEmpty()) executed.pop().undo(); }
}
11. Explain how java.io.Serializable can be used to implement a Memento, and discuss when serialization-based snapshots are the right implementation choice versus a hand-written copy.
If the Originator's state graph implements Serializable, you can build a memento by serializing the Originator's fields to a byte array and later deserializing that array to restore the state; this automatically performs a deep copy of the entire object graph without hand-writing per-field copy logic. It is the right choice when the state graph is large, deeply nested, or already Serializable for other reasons (such as HTTP session replication), and when snapshots must be persisted to disk or shipped across a process boundary.
class SerializingMementoFactory {
static byte[] capture(Serializable state) {
try (var bos = new java.io.ByteArrayOutputStream();
var oos = new java.io.ObjectOutputStream(bos)) {
oos.writeObject(state);
return bos.toByteArray();
} catch (java.io.IOException e) {
throw new IllegalStateException("Failed to capture state", e);
}
}
static Object restore(byte[] snapshot) {
try (var ois = new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(snapshot))) {
return ois.readObject();
} catch (java.io.IOException | ClassNotFoundException e) {
throw new IllegalStateException("Failed to restore state", e);
}
}
}
The trade-off is speed and fragility: serialization is slower than direct field copying and breaks if the class's serializable shape changes between capture and restore.
12. Compare using Cloneable and a copy constructor as a lightweight alternative to writing a dedicated Memento class, and explain when this simplification is appropriate.
When the Originator's state is a single flat, cloneable value object and there is no need to hide the snapshot's shape from the Caretaker (for example, an internal utility class only your own undo code will ever touch), you can skip the dedicated Memento type and simply store deep clones of the Originator itself, using a copy constructor or clone().
class Shape implements Cloneable {
int x, y, width, height;
@Override public Shape clone() {
try { return (Shape) super.clone(); }
catch (CloneNotSupportedException e) { throw new AssertionError(e); }
}
}
// Caretaker just stores clones directly, no separate Memento type
java.util.Deque<Shape> history = new java.util.ArrayDeque<>();
history.push(shape.clone());
This is simpler but weaker on encapsulation: the Caretaker now holds a full-fidelity copy of the Originator's real type, with all of its public methods callable, rather than an opaque snapshot with no exposed behavior. Reach for a dedicated Memento class once other code, or a different team, needs to hold onto the history.
13. Explain how a database transaction savepoint is a Memento-like mechanism at the persistence layer, and how it differs from an application-level Memento object.
A SAVEPOINT lets a transaction mark a point it can later roll back to without aborting the entire transaction, conceptually the same idea as capturing a memento of the database's state and restoring it on demand. The database engine is effectively acting as the Originator, an internal transaction log entry is the Memento, and your application code, calling rollback(savepoint), is the Caretaker.
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
Savepoint sp = conn.setSavepoint("beforeRiskyUpdate");
try {
performRiskyUpdate(conn);
conn.commit();
} catch (SQLException e) {
conn.rollback(sp); // restore to the memento-like savepoint
}
Unlike an application-level Memento, a savepoint only covers what the database transaction touched; it cannot undo in-memory object state or external side effects like an email already sent, which is why the two are often combined rather than used as substitutes for each other.
14. Design a game "save state" / checkpoint system using the Memento pattern, where a player can save progress at a checkpoint and later reload from any saved slot.
The game world (level, player position, inventory, health) is the Originator; a save snapshot is the Memento; a SaveSlotManager Caretaker stores named or numbered slots, typically serialized to disk so progress survives a restart.
class GameState {
interface Save {}
private static final class Snapshot implements Save, Serializable {
final int level; final int hp; final java.util.List<String> inventory;
Snapshot(int level, int hp, java.util.List<String> inventory) {
this.level = level; this.hp = hp; this.inventory = java.util.List.copyOf(inventory);
}
}
private int level, hp;
private java.util.List<String> inventory = new java.util.ArrayList<>();
Save save() { return new Snapshot(level, hp, inventory); }
void load(Save s) {
Snapshot snap = (Snapshot) s;
this.level = snap.level; this.hp = snap.hp;
this.inventory = new java.util.ArrayList<>(snap.inventory);
}
}
15. How would you use the Memento pattern to implement a "go back a step" feature in a multi-step form wizard, where each step's data must be preserved exactly as the user left it?
Treat the wizard controller as the Originator holding the current step's field values, capture a Memento before advancing to the next step, and push it onto a per-session Caretaker stack. Pressing "back" pops the previous step's memento and restores those exact field values, including anything the user had already typed, rather than resetting the step to blank defaults.
class WizardStep {
private java.util.Map<String, String> fields = new java.util.HashMap<>();
Memento snapshot() { return new Memento(new java.util.HashMap<>(fields)); }
void restore(Memento m) { this.fields = new java.util.HashMap<>(m.fields); }
static final class Memento {
private final java.util.Map<String, String> fields;
private Memento(java.util.Map<String, String> fields) { this.fields = fields; }
}
}
16. Explain the subtle but important distinction between the Memento pattern and the Prototype pattern, given both involve copying an object's state.
Prototype is about creating a brand-new, independent object by copying an existing one, typically so the copy can then diverge and live its own life as a separate instance. Memento is about capturing one specific object's state at a point in time so that same object, later, can have its state put back to exactly what it was; the snapshot is not meant to become a new independent object that the rest of the system starts using.
In other words, Prototype answers "give me a new object like this one," while Memento answers "let this exact object undo what just happened to it." A Memento's snapshot is typically opaque and useless outside of being handed back to its own Originator, whereas a Prototype's clone is a fully functional peer object.
17. Explain the relationship between the Memento pattern and the Command pattern, and why they are so frequently used together to implement undo functionality.
Command encapsulates an action as an object with execute(), which is a natural place to also add undo(); but undoing an arbitrary action generically requires knowing the state to go back to, which is exactly what Memento supplies. Command provides the "what happened and how to reverse it" API, while Memento provides the actual "reversed-to" state data, so combining them gives you an undo system that works for any action without each Command needing to hand-write field-by-field undo logic.
They are not the same pattern: Command's core concern is encapsulating a request as an object (useful for queuing, logging, or macros even without undo), while Memento's core concern is encapsulation-safe state capture (useful even without any notion of "commands" at all, such as a simple checkpoint/restore feature).
18. Explain how the Memento pattern differs from the State pattern, since both patterns are concerned with an object's internal state.
State is about changing an object's behavior based on which "mode" or state it is currently in, by delegating to different state-specific strategy objects, so the object appears to change its class at runtime. Memento is about capturing a snapshot of an object's state so it can be restored later; it says nothing about varying behavior and is not concerned with the object switching between named states at all, only with preserving and restoring whatever its state happens to be.
A useful contrast: a traffic light using State pattern swaps its "current state" strategy object as it cycles red/yellow/green; a traffic light using Memento would let you save "it was red with 12 seconds left" and later restore exactly that, regardless of how many states the system has.
19. Describe how to write unit tests for memento-based undo, including asserting deep equality between the state before capture, after mutation, and after restore.
A solid test captures the state, mutates the Originator, restores from the memento, and asserts the restored state is deeply equal, field by field including nested mutable collections, to the state at capture time, not merely reference-equal or superficially equal. It is also worth asserting that the Originator's state genuinely changed between capture and restore, otherwise the test could pass even if restore silently did nothing.
@Test
void restoreBringsBackExactPriorState() {
Document doc = new Document();
doc.type("hello");
Document.Memento saved = doc.snapshot();
doc.type(" world");
assertThat(doc.getText()).isEqualTo("hello world"); // sanity: mutation happened
doc.restore(saved);
assertThat(doc.getText()).isEqualTo("hello");
}
For nested mutable fields such as a list, also mutate the live object's list after restoring and confirm the restored snapshot's own list is unaffected, which catches the classic shallow-copy bug.
20. Discuss the thread-safety concerns when multiple threads may create or restore mementos concurrently against the same Originator, and how you would make this safe in Java.
If one thread is capturing a memento while another thread is mutating the Originator's fields, the snapshot can end up torn, some fields reflecting the state before the mutation and others reflecting it mid-mutation. Guard both createMemento() and restore() with the same lock used to guard ordinary mutating methods, so a snapshot is always taken, or applied, atomically with respect to concurrent writers.
class ThreadSafeCounter {
private final Object lock = new Object();
private int value;
Memento snapshot() { synchronized (lock) { return new Memento(value); } }
void restore(Memento m) { synchronized (lock) { this.value = m.value; } }
void increment() { synchronized (lock) { value++; } }
static final class Memento { private final int value; private Memento(int v) { value = v; } }
}
21. Describe a common production bug where a shallow-copied memento shares mutable references with the live Originator, causing "undo" to appear to do nothing.
If the memento stores the same List or other mutable object reference the Originator holds, rather than an independent copy, then mutating the Originator's live object also mutates what the memento is "remembering," since they point at the same underlying data. When restore later copies that reference back, it copies a reference to data that has already been mutated in place, so the document looks unchanged, defeating the entire purpose of the undo.
// BUG: shares the same list instance
static final class BuggyMemento {
final java.util.List<String> items;
BuggyMemento(java.util.List<String> items) { this.items = items; } // no copy!
}
// FIX: defensively copy at capture time
static final class SafeMemento {
final java.util.List<String> items;
SafeMemento(java.util.List<String> items) { this.items = new java.util.ArrayList<>(items); }
}
22. Describe a bug where a memento's contents leak through an incorrectly scoped accessor, breaking the pattern's encapsulation guarantee.
If the Memento's field-reading method is declared public instead of private, or the nested Memento class itself is declared public instead of private, then any code holding a reference, including the Caretaker, can read or even mutate the snapshot's internals directly, exactly what the pattern is designed to prevent.
// BUG: public accessor exposes internals to anyone holding the memento
static class LeakyMemento {
public String getSecretState() { return state; } // Caretaker can now read this!
private final String state;
LeakyMemento(String state) { this.state = state; }
}
This class of bug is dangerous precisely because it compiles fine and often works correctly at first; the encapsulation break only matters once some other developer, seeing the "harmless" getter, starts depending on it, coupling unrelated code to the Originator's internal shape.
23. Explain event sourcing as a large-scale alternative to storing periodic full-state mementos, and how the two ideas relate to each other.
Instead of periodically snapshotting an entire object's state, event sourcing records every state-changing event as an immutable, ordered log, and rebuilds current state by replaying events from the beginning (or from the last snapshot). A periodic full-state snapshot in an event-sourced system is, structurally, exactly a Memento: an opaque capture of an aggregate's state that lets you skip replaying the entire event history from day one.
class OrderAggregate {
private OrderState state = OrderState.empty();
void apply(OrderEvent event) { state = state.applying(event); }
// A "memento" at scale: skip replaying from event #1
OrderSnapshot snapshot() { return new OrderSnapshot(state); }
void restoreFrom(OrderSnapshot snap) { this.state = snap.state(); }
}
The trade-off is different too: event sourcing gives you a full audit trail and the ability to reconstruct any historical point, at the cost of replay time, which snapshotting (Memento at scale) exists specifically to bound.
24. Walk through a UML-style class diagram for the Memento pattern, describing each association: Originator to Memento, and Caretaker to Memento.
The Originator has a dependency on the Memento class, since it creates instances of it and knows its internal shape, typically modeled as the Originator containing the Memento as a private nested class. The Caretaker has an association to Memento too, but only through the narrow interface, typically drawn as holding a collection of an opaque Memento marker type, with no dependency arrow toward the concrete implementation class at all.
- Originator — depends on the concrete Memento type; can read and write its full state.
- Memento — depends on nothing; a passive, mostly immutable data holder.
- Caretaker — depends only on the narrow Memento interface; stores instances, never inspects them.
25. Describe the "marker interface" technique from the original Gang of Four Memento description, where the Caretaker only ever sees an empty public interface type.
A marker interface with no methods at all, such as interface Memento {}, is the simplest way to give the Caretaker a type it can declare variables and collections of, while guaranteeing at compile time that it cannot call any method on that type, since there are none to call.
public interface Memento {} // marker: zero methods, purely a type token
class Originator {
private static final class ConcreteMemento implements Memento {
final String state;
ConcreteMemento(String state) { this.state = state; }
}
Memento save() { return new ConcreteMemento(currentState()); }
}
class Caretaker {
private final java.util.List<Memento> history = new java.util.ArrayList<>(); // opaque type only
}
26. Discuss the trade-offs between implementing Memento as a static nested class of the Originator versus a top-level standalone class, in terms of encapsulation and testability.
A private static nested class gives the strongest encapsulation, since only the enclosing Originator can construct or read it, at the cost of making the Memento's fields harder to unit test directly, forcing tests to go through the Originator's public save/restore API instead. A top-level class is easier to test in isolation and to reuse across multiple Originators sharing the same state shape, but it must rely on access-modifier discipline (package-private fields, no public getters) rather than the compiler's nested-class visibility rules to keep the Caretaker out.
Most production code favors the nested-class style for a single-purpose Originator, and a top-level Memento only when the same snapshot shape is genuinely reused by more than one class.
27. Design a Google-Docs-style version history feature for a collaborative document editor using the Memento pattern, including how a user could preview and restore an older version.
Every save (manual or auto-save on a timer) captures a Memento tagged with a timestamp and author, appended to an append-only history list rather than a simple undo stack, since the user should be able to jump to any past version, not just step back one at a time. "Preview" temporarily restores a chosen memento into a read-only view without popping it off the history, and "restore this version" both restores it into the live document and appends a new memento marking that restoration as a fresh history entry, so the action itself is undoable too.
record VersionEntry(java.time.Instant savedAt, String author, Document.Memento snapshot) {}
class VersionHistory {
private final java.util.List<VersionEntry> entries = new java.util.ArrayList<>();
void record(String author, Document.Memento m) {
entries.add(new VersionEntry(java.time.Instant.now(), author, m));
}
}
28. How would you implement a "revert to previous configuration" feature for an application's settings screen using the Memento pattern?
Capture a Memento of the current settings object the moment the settings screen is opened, before any field is changed; if the user cancels or clicks "revert," restore that single memento, discarding whatever edits were made in between. If settings are saved incrementally (applied as the user changes each toggle, for live preview) rather than only on a final "Save" button, keep a small history so "revert" can step back through several applied changes, not just to the screen-open snapshot.
class SettingsController {
private final AppSettings settings;
private AppSettings.Memento onOpen;
void screenOpened() { onOpen = settings.snapshot(); }
void revert() { settings.restore(onOpen); }
}
29. Design an undo feature for a chess (or similar board game) application using the Memento pattern, capturing the board position and game metadata needed to reverse a move.
The board state, whose turn it is, castling rights, and en passant eligibility all matter for correctness, so the memento must capture all of them, not just piece positions, or "undo" could produce an illegal position (for example, allowing castling that should no longer be legal). A move-history Caretaker stack lets players step back through an entire game, one move at a time.
class ChessBoard {
static final class Memento {
final Piece[][] squares; final Color turn; final CastlingRights rights; final Square enPassantTarget;
Memento(Piece[][] squares, Color turn, CastlingRights rights, Square enPassantTarget) {
this.squares = deepCopy(squares); this.turn = turn; this.rights = rights; this.enPassantTarget = enPassantTarget;
}
}
}
30. Design an undo system for a graphics/vector editor where each shape has position, size, color, and style properties, all of which must be restorable.
Each shape is an Originator capable of snapshotting its own geometric and style properties; the canvas-level Caretaker, however, typically needs to undo operations that touch multiple shapes at once (a multi-select drag or a bulk recolor), so the canvas captures a composite memento containing one per-shape memento for every shape affected by the operation, not just a single shape's snapshot.
class CanvasMemento {
private final java.util.Map<String, Shape.Memento> byShapeId;
CanvasMemento(java.util.Map<String, Shape.Memento> byShapeId) {
this.byShapeId = java.util.Map.copyOf(byShapeId);
}
}
31. How would you use the Memento pattern to implement rollback of a partially completed, multi-field form validation flow, where an invalid submission should not leave the form in a corrupted intermediate state?
Snapshot the form's field values before applying any auto-correction or normalization logic during validation (such as trimming whitespace, reformatting a phone number, or auto-capitalizing a name); if validation ultimately fails for reasons unrelated to those normalized fields, restore the pre-normalization memento so the user sees exactly what they originally typed rather than a partially transformed value.
Form.Memento before = form.snapshot();
form.normalizeFields();
ValidationResult result = validator.validate(form);
if (!result.isValid()) {
form.restore(before); // don't show the user silently-mutated input
}
32. Explain why mementos are typically designed to be immutable once created, and what problems can arise if a memento's fields are mutable and shared.
An immutable memento can be safely stored, copied between threads, and reused any number of times without worrying that something has quietly changed its contents between capture and restore. If a memento's fields are mutable and something outside the Originator (even accidentally, through a leaked reference) mutates them, restoring from that memento no longer reproduces the state that was actually captured, silently corrupting the undo history.
// Immutable memento: fields are final, defensively copied at construction
static final class Memento {
private final java.util.List<String> items; // stored as an unmodifiable defensive copy
Memento(java.util.List<String> items) { this.items = java.util.List.copyOf(items); }
}
33. Discuss whether a Memento class should implement equals() and hashCode(), and how doing so helps or complicates testing.
Implementing value-based equals()/hashCode() on a Memento (comparing captured field values) makes tests much cleaner, since you can assert assertThat(restoredMemento).isEqualTo(originalMemento) instead of hand-comparing each field. The main complication is that a Memento's fields are often meant to be inaccessible outside the Originator, so equals() must be implemented from inside the Originator's own nested class, which is fine, but it does mean the equality contract is now part of the Originator's private implementation detail rather than a public API guarantee.
static final class Memento {
private final String text;
private Memento(String text) { this.text = text; }
@Override public boolean equals(Object o) {
return o instanceof Memento m && java.util.Objects.equals(text, m.text);
}
@Override public int hashCode() { return java.util.Objects.hashCode(text); }
}
34. Describe the memory leak risk of a Caretaker that keeps an unbounded list of mementos for the lifetime of a long-running application, and how you would detect and fix it.
If every edit pushes a new memento and nothing ever evicts old ones, memory usage grows linearly with the number of edits made over the application's entire lifetime, which for a long-running desktop session or server-side editor process can eventually exhaust the heap. You would typically detect this through a heap dump or memory profiler showing a steadily growing collection retained by the Caretaker, then fix it by bounding history size, evicting oldest entries, or switching from full snapshots to lightweight diffs.
35. Discuss using java.lang.ref.WeakReference (or SoftReference) for stored mementos to let the JVM reclaim old undo history under memory pressure, and the trade-offs of doing so.
Wrapping older mementos in a SoftReference lets the garbage collector reclaim them if the JVM is genuinely low on memory, before throwing OutOfMemoryError, which is attractive for "nice to have but not essential" deep undo history. The trade-off is that undo can then silently fail, or jump further back than the user expects, once the soft references have been cleared, which is a confusing user experience if not clearly communicated (for example, graying out undo entries that have been reclaimed).
java.util.Deque<java.lang.ref.SoftReference<Document.Memento>> history = new java.util.ArrayDeque<>();
Most applications prefer an explicit bounded history over relying on soft references, since it gives predictable, testable behavior instead of GC-dependent behavior.
36. Explain how you would serialize mementos to disk to support crash recovery, so a user can resume unsaved work after an application crash or unexpected shutdown.
Periodically (for example, every few seconds of inactivity, or after N edits) serialize the current memento to a recovery file on disk; on next startup, check whether a recovery file exists and, if so, offer to restore from it before the user starts a fresh session. Delete the recovery file on a clean, explicit save or exit so it does not incorrectly resurrect stale work on the next normal launch.
void autoSaveRecoveryPoint(Document doc) {
byte[] snapshot = SerializingMementoFactory.capture(doc.snapshot());
java.nio.file.Files.write(recoveryFilePath(), snapshot);
}
37. Explain how the Memento pattern is often combined with the Observer pattern to let a UI enable or disable its Undo and Redo buttons based on whether history is available.
The Caretaker, after pushing or popping a memento, notifies registered observers (typically the toolbar or menu controller) whenever the undo stack becomes empty, non-empty, or the redo stack changes, so the UI can gray out or enable the corresponding buttons without polling the stack's state on every render.
interface HistoryListener { void onHistoryChanged(boolean canUndo, boolean canRedo); }
class ObservableUndoManager {
private final java.util.List<HistoryListener> listeners = new java.util.ArrayList<>();
private final java.util.Deque<Document.Memento> undoStack = new java.util.ArrayDeque<>();
private final java.util.Deque<Document.Memento> redoStack = new java.util.ArrayDeque<>();
void addListener(HistoryListener l) { listeners.add(l); }
private void notifyListeners() {
for (HistoryListener l : listeners) l.onHistoryChanged(!undoStack.isEmpty(), !redoStack.isEmpty());
}
}
38. Explain diff-based (delta) mementos as a memory optimization technique for large documents, storing only what changed rather than a full copy at every step.
Instead of storing a complete copy of a potentially large document at every edit, a diff-based memento stores just the delta, the minimal edit operation (insert, delete, replace at a given range) needed to go from the previous state to the current one. Restoring requires walking the chain of deltas and applying (or reverse-applying) them in order, which trades a small amount of restore-time CPU work for a large reduction in memory footprint.
record TextDelta(int position, String removed, String inserted) {
String applyTo(String text) {
return text.substring(0, position) + inserted + text.substring(position + removed.length());
}
String reverseApplyTo(String text) {
return text.substring(0, position) + removed + text.substring(position + inserted.length());
}
}
39. Explain copy-on-write as a technique to make snapshot creation cheap even for large Originator state, and how it applies to implementing efficient mementos.
Rather than eagerly deep-copying every field the instant a memento is requested, a copy-on-write memento shares the underlying data structure with the live Originator until either side actually attempts a mutation, at which point that side transparently copies just the portion being changed. This makes creating a memento essentially free when no further mutation happens, which matters for high-frequency snapshotting such as capturing state before every keystroke.
// Persistent (structurally shared) list avoids a full array copy per snapshot
io.vavr.collection.List<String> before = currentLines; // O(1), no copy
currentLines = currentLines.append("new line"); // shares old nodes, allocates only new ones
40. Discuss using persistent (immutable, structurally shared) data structures as a way to avoid the cost of full copying every time a memento is created.
Persistent data structures, such as those found in libraries like Vavr, share unchanged parts of their internal tree or list representation between the old and new versions after a modification, so "keeping the old version around as a memento" costs only the small amount of newly allocated structure, not a full copy of the whole collection. This makes them a natural fit for Originators whose state includes large collections that would otherwise be expensive to snapshot on every change.
The trade-off is that persistent collections generally have different (sometimes higher) per-operation costs than plain mutable Java collections, so this technique pays off specifically when snapshot frequency is high relative to how often the data is read.
41. Explain how the idea of snapshotting state, as in the Memento pattern, extends to capturing and later restoring state across multiple services in a distributed system.
At the scale of a distributed system, "the Originator" is really a set of cooperating services, and a single-object memento is not enough; you typically need a coordinated snapshot across each service's own state (each acting as a mini-Originator), often correlated by a shared checkpoint identifier or timestamp, so that restoring any one service alone does not leave the overall system in an inconsistent cross-service state.
This is genuinely harder than the classic Memento pattern because of network partitions and independent failure of each service; solutions typically borrow from distributed snapshot algorithms (such as Chandy-Lamport) rather than a naive "call snapshot() on every service and hope they all succeed."
42. Compare database snapshot isolation (as a concurrency-control mechanism) to the application-level Memento pattern, clarifying that they solve different problems despite similar-sounding names.
Snapshot isolation is a transaction isolation level that gives each transaction a consistent view of the database as of the moment it started, so concurrent writers do not see each other's uncommitted changes; its purpose is concurrency control, not deliberate rollback-on-demand by application code. Memento, by contrast, is an explicit, intentional capture-and-restore mechanism triggered by application logic, unrelated to how the database isolates concurrent transactions from one another.
They can coexist: a database running under snapshot isolation might still be the backing store for an application that separately implements Memento-based undo at the object level.
43. Design an undo/redo system for a drawing/canvas application using Memento and Command together, where operations include draw, move, resize, and delete.
Each drawing operation is a Command that, on execute, captures a Memento of the affected shape (or, for delete, the shape itself plus its position in the z-order) before mutating the canvas, and on undo simply restores that memento. A single CommandHistory Caretaker stack then supports undoing any operation type uniformly, without the history manager needing separate logic per operation kind.
class DeleteShapeCommand implements Command {
private final Canvas canvas;
private final String shapeId;
private Canvas.Memento before;
DeleteShapeCommand(Canvas canvas, String shapeId) { this.canvas = canvas; this.shapeId = shapeId; }
@Override public void execute() { before = canvas.snapshot(); canvas.remove(shapeId); }
@Override public void undo() { canvas.restore(before); }
}
44. How would you coordinate undo across multiple independent Originators involved in a single logical transaction, so undoing "one action" correctly reverses changes to all of them together?
Wrap the individual mementos from each affected Originator inside one composite memento representing the whole transaction, and have the Caretaker treat that composite as a single history entry; restoring it must restore every contained memento to its respective Originator, ideally as one atomic step so you never end up with only some of the Originators rolled back.
class TransactionMemento {
private final java.util.Map<Originator, Object> mementosByOriginator;
TransactionMemento(java.util.Map<Originator, Object> mementosByOriginator) {
this.mementosByOriginator = java.util.Map.copyOf(mementosByOriginator);
}
void restoreAll() {
mementosByOriginator.forEach(Originator::restore);
}
}
45. Design nested or composite mementos for undoing a change to a Composite-pattern object tree, such as a folder containing files and subfolders.
A composite memento for a tree structure typically mirrors the tree's own shape: a folder's memento contains its own metadata plus a list of child mementos, recursively, one per child node, so restoring a folder's memento recursively restores every descendant to the state it had when the snapshot was taken.
class FolderMemento {
final String name;
final java.util.List<Object> childMementos; // each child's own memento, recursively
FolderMemento(String name, java.util.List<Object> childMementos) {
this.name = name; this.childMementos = java.util.List.copyOf(childMementos);
}
}
46. Explain how javax.swing.undo.UndoManager provides a ready-made Memento-and-Command-style undo/redo framework in the standard JDK, and how you would wire your own edits into it.
UndoManager is a Caretaker: it holds a bounded, linear history of UndoableEdit objects (Command-like objects that also know how to undo/redo themselves) and exposes undo()/redo() plus canUndo()/canRedo(). Your code creates an UndoableEdit for each user action and hands it to the manager via addEdit; the manager itself never inspects the edit's private state, only calls its public undo()/redo() methods.
javax.swing.undo.UndoManager undoManager = new javax.swing.undo.UndoManager();
// wire the manager in as a listener; each edit event carries its own UndoableEdit
textDocument.addUndoableEditListener(e -> undoManager.addEdit(e.getEdit()));
void undo() { if (undoManager.canUndo()) undoManager.undo(); }
void redo() { if (undoManager.canRedo()) undoManager.redo(); }
47. Explain the roles of UndoableEdit and StateEditable in the javax.swing.undo package as a real-world Java library implementation related to the Memento pattern.
UndoableEdit is closer to Command (it knows how to undo and redo itself), but implementations frequently rely on Memento internally, capturing the before/after state needed to perform that undo or redo. StateEditable takes the pure-Memento angle further: it lets an object expose storeState(Hashtable) and restoreState(Hashtable), letting the framework's StateEdit class snapshot and restore arbitrary object state generically, which is functionally a Memento capture/restore pair with a hashtable as the snapshot's storage medium.
48. How do you handle schema versioning for a Memento when the Originator's own fields change shape over time, such as adding a new field in a later software release?
Tag each memento with a version number written at capture time; when restoring an older-versioned memento, run it through a migration step that fills in sensible defaults for fields that did not exist yet, rather than assuming every stored memento matches the current field layout exactly. This matters most for persisted mementos, such as saved games or documents restored across application upgrades, where old snapshots can otherwise fail to load or silently corrupt state after a schema change.
record VersionedSnapshot(int schemaVersion, java.util.Map<String, Object> fields) {}
Object migrate(VersionedSnapshot snap) {
var fields = new java.util.HashMap<>(snap.fields());
if (snap.schemaVersion() < 2) fields.putIfAbsent("difficulty", "NORMAL"); // field added in v2
return fields;
}
49. Compare deep-cloning the Originator's fields directly versus fully serializing and deserializing the Originator to build a memento, in terms of correctness and performance.
A hand-written deep clone is typically faster and gives you precise control over exactly which fields get copied and how, but it requires writing (and maintaining) correct deep-copy logic for every mutable field, which is easy to get subtly wrong as the class evolves. Serialization-based deep copying is slower and requires the whole state graph to be Serializable, but it automatically stays correct as fields are added, since the serialization mechanism walks the object graph generically rather than relying on hand-maintained copy code.
A common middle ground is to hand-write copying for small, frequently snapshotted state, and fall back to serialization only for large, infrequently snapshotted, or already-serializable state graphs.
50. Discuss using Java records to represent immutable memento snapshots in modern Java, and whether records make a dedicated Memento class unnecessary.
A record is a natural fit for a memento's data shape: it is immutable by default, generates equals()/hashCode()/toString() for free, and its compact constructor is a convenient place to defensively copy any mutable field passed in. Records do not eliminate the need for the encapsulation discipline of Memento, though; a public record is just as visible to the Caretaker as a public class, so keeping it private (or nested and access-restricted) is still necessary if hiding internals from the Caretaker matters.
class Document {
private record Memento(String text, int cursor) {
Memento { /* compact constructor: validate or defensively copy if needed */ }
}
}
51. Discuss combining the Builder pattern with Memento so that restoring a complex Originator's state is done through a fluent, validated builder rather than directly overwriting fields.
When restoring a memento requires re-running validation or derived-field computation, rather than a blind field-for-field copy, pass the memento's captured values into a Builder that reconstructs a fully valid Originator state, catching any inconsistency introduced by, for example, a corrupted persisted snapshot, instead of silently accepting invalid data.
void restore(Memento m) {
OrderState restored = OrderState.builder()
.items(m.items())
.total(m.total())
.build(); // builder re-validates invariants, e.g. total matches sum of items
this.state = restored;
}
52. Discuss the security implications of a memento accidentally capturing sensitive data, such as a plaintext password field, inside an undo history that might later be logged or persisted.
If an Originator's state includes a sensitive field, such as a password confirmation input on a signup form, a naive full-state memento will capture that value too, and if the undo history is ever serialized to disk, sent in a crash report, or logged for debugging, the sensitive value leaks along with it. Exclude sensitive fields from the snapshot entirely, or store only a masked/hashed placeholder, and never let a memento's toString() print raw sensitive fields.
53. Discuss the performance cost of capturing a full memento on every single keystroke in a text editor, and why this quickly becomes impractical at scale.
Capturing (and deep-copying) the entire document's text on every keystroke means an O(document length) allocation and copy per character typed, which for a large document turns fast typing into a visibly laggy experience and floods the undo history with an enormous number of near-duplicate snapshots, most of which the user will never actually want to undo to individually.
Production editors instead group rapid keystrokes into a single undo unit (for example, one memento per "word typed" or per pause in typing), which both improves performance and produces a more useful, less granular undo history from the user's point of view.
54. Describe a debounce/throttle strategy for creating mementos during rapid, continuous edits, so undo history captures meaningful checkpoints rather than every micro-change.
Rather than snapshotting on every keystroke, start (or reset) a short timer on each edit and only push a memento once the timer elapses without further edits, effectively coalescing a burst of rapid typing into a single undo step; also force a snapshot boundary on structurally significant events, like inserting a newline or pasting a block of text, regardless of the timer.
class DebouncedSnapshotter {
private final long quietPeriodMs;
private long lastEditAt;
private Document.Memento pending;
void onEdit(Document doc) {
lastEditAt = System.currentTimeMillis();
pending = doc.snapshot(); // captured, but not yet committed to history
}
void maybeCommit(UndoStack stack) {
if (pending != null && System.currentTimeMillis() - lastEditAt >= quietPeriodMs) {
stack.push(pending);
pending = null;
}
}
}
55. Compare using the Memento pattern for undo versus using CRDTs (Conflict-free Replicated Data Types) in a real-time collaborative editor with multiple simultaneous authors.
Memento-based undo works cleanly for a single author's own linear history, restoring exactly the state that author's own edits produced, but it does not by itself resolve the problem of merging concurrent edits from multiple collaborators editing the same document simultaneously. CRDTs are designed specifically for that concurrent-merge problem, guaranteeing all replicas converge to the same state regardless of network delay or ordering, but per-author "undo my last change" on top of a CRDT is a genuinely harder problem than on a single-author Memento stack, since undoing one author's operation might now conflict with edits other authors made afterward, based on that operation.
In practice, collaborative editors often use CRDTs (or operational transform) for the merge problem and layer a Memento- or Command-based local undo experience on top, scoped per author.
56. Discuss implementing an undo middleware for a Redux/Flux-style Java state container, where every dispatched action's resulting state change should be undoable.
An undo middleware intercepts the state container's reducer pipeline: before a new state is computed and applied, it pushes the current (pre-action) state onto a history stack as a memento, so undoing simply pops the previous state and republishes it to all subscribers, without any single reducer needing its own undo logic.
class UndoMiddleware<S> {
private final java.util.Deque<S> history = new java.util.ArrayDeque<>();
S beforeDispatch(S currentState) {
history.push(currentState);
return currentState;
}
S undo(S fallback) { return history.isEmpty() ? fallback : history.pop(); }
}
57. What edge cases should be tested for a Memento-based undo/redo Caretaker, such as calling undo on an empty history or redo when there is nothing to redo?
Calling undo when the history is empty must be a safe no-op, never throwing or corrupting state; the same applies to redo when the redo stack is empty. Also test that a fresh edit after several undos correctly clears the redo stack, that undoing all the way back reaches the original, pre-edit state exactly, and that repeatedly undoing past the oldest available memento (in a bounded history) stops at the oldest retained snapshot rather than throwing.
@Test
void undoOnEmptyHistoryIsNoOp() {
UndoStack stack = new UndoStack();
Document doc = new Document();
doc.type("safe");
stack.undo(doc); // nothing recorded yet
assertThat(doc.getText()).isEqualTo("safe");
}
58. Design a "preview and cancel" feature using Memento, where a change is tentatively applied and can be reverted with certainty if the user does not confirm it.
Snapshot the Originator's state immediately before applying the tentative change; if the user confirms, simply discard the snapshot; if the user cancels (or navigates away, or an explicit timeout elapses), restore from that snapshot to guarantee the Originator ends up exactly as it was, rather than trying to manually reverse whatever the tentative change happened to do.
Document.Memento beforePreview = doc.snapshot();
doc.applyTentativeTheme(darkTheme);
// user clicks "Cancel":
doc.restore(beforePreview);
// user clicks "Apply": simply do nothing further, beforePreview is discarded
59. How would you design a Caretaker that supports jumping directly to an arbitrary point in history, such as clicking entry #12 in a visible list of past edits, rather than only single-step undo/redo?
Instead of a strict stack, store history as an indexable list with a current-position pointer; "jump to entry N" restores that entry's memento directly and moves the pointer to N, while normal undo/redo simply decrement or increment the pointer by one and restore the memento at the new position.
class IndexedHistory {
private final java.util.List<Document.Memento> entries = new java.util.ArrayList<>();
private int position = -1;
void record(Document.Memento m) { entries.add(m); position = entries.size() - 1; }
void jumpTo(int index, Document doc) {
position = index;
doc.restore(entries.get(position));
}
}
60. Design a Caretaker using an LRU (least-recently-used) eviction strategy for stored mementos, rather than simple oldest-first eviction, and explain when LRU is the better fit.
Simple oldest-first eviction assumes the most recently created memento is always the most valuable, which is true for a strict linear undo stack, but if the Caretaker also supports jumping to arbitrary history points (bookmarked checkpoints, say), some old entries may be revisited often and deserve to be kept longer than newer, never-revisited ones; an LRU cache (such as a LinkedHashMap in access-order mode) evicts based on actual usage instead of pure age.
class LruMementoStore extends java.util.LinkedHashMap<String, Document.Memento> {
private final int maxEntries;
LruMementoStore(int maxEntries) { super(16, 0.75f, true); this.maxEntries = maxEntries; }
@Override protected boolean removeEldestEntry(java.util.Map.Entry<String, Document.Memento> eldest) {
return size() > maxEntries;
}
}
61. Explain how to safely snapshot an object graph that contains circular references inside a memento, without the capture process recursing infinitely.
Maintain an identity map from each already-visited original object to the clone already created for it; before deep-copying a referenced object, check this map first, and if a clone already exists, reuse that same clone reference instead of recursing again, which both breaks the infinite recursion and preserves the original graph's sharing structure inside the copy.
java.util.IdentityHashMap<Object, Object> alreadyCopied = new java.util.IdentityHashMap<>();
Object deepCopy(Object original) {
if (alreadyCopied.containsKey(original)) return alreadyCopied.get(original);
Object copy = shallowCopyShellOf(original);
alreadyCopied.put(original, copy); // register before recursing into children
copyFieldsRecursively(original, copy, alreadyCopied);
return copy;
}
62. Discuss efficiently handling large binary payloads, such as an embedded image, inside a memento without duplicating the bytes on every snapshot.
If the binary payload itself is immutable once set (a user pastes an image, and it is never edited in place, only replaced wholesale), the memento can simply hold the same reference to that immutable byte array or image object rather than copying the bytes, since there is no risk of the shared data being mutated later. Only fall back to an actual copy when the payload type is mutable, or when you cannot prove it will never be mutated in place after the snapshot.
static final class Memento {
final String text;
final byte[] immutableImageBytes; // safe to share: never mutated after creation
Memento(String text, byte[] immutableImageBytes) { this.text = text; this.immutableImageBytes = immutableImageBytes; }
}
63. Discuss reusing Prototype-style deep-copy logic inside a Memento's own capture step, so the same tested cloning code serves both patterns.
If the Originator's state is itself a graph of Cloneable objects with well-tested deep-clone logic (built for Prototype elsewhere in the codebase), a Memento's createMemento() can simply call that existing deep-clone code to build the snapshot's contents, rather than re-implementing field-by-field copying a second time.
Memento snapshot() {
return new Memento(this.settings.deepClone()); // reuses Prototype-style clone()
}
This is a nice example of two patterns solving different problems (new independent object vs. restorable snapshot) while still sharing the underlying "how do I correctly deep-copy this data" implementation.
64. Describe how the Memento pattern can be used in tests to roll back a finite-state machine to a known state between test cases, rather than reconstructing the machine from scratch each time.
Build the state machine once, with whatever expensive setup that requires, capture a memento of its initial state, and at the start of each test restore from that memento instead of rebuilding the machine, which can be significantly faster for state machines with costly construction while still guaranteeing each test starts from an identical, known-good state.
@BeforeEach
void resetMachine() {
machine.restore(initialSnapshot); // faster than new StateMachine(...) per test
}
65. Explain the technique of exposing a package-private "wide" accessor method on a Memento for the Originator's use, distinct from the public "narrow" interface seen by other packages.
When the Memento and Originator live in the same package but you still want a compile-time boundary against unrelated code in other packages, declare the Memento's read methods package-private rather than fully private; the Originator (same package) can call them freely, while a Caretaker defined in a different package cannot, even if it holds a direct reference to the concrete Memento type.
package com.example.editor;
class DocumentMemento {
private final String text;
DocumentMemento(String text) { this.text = text; }
String getText() { return text; } // package-private: visible only within com.example.editor
}
66. Discuss designing a generic Memento<T> interface in Java that can be reused across multiple different Originator types, and its limitations.
A generic wrapper such as Memento<T> holding a captured value of type T can reduce boilerplate for simple Originators whose entire state is one value object, letting a single generic Caretaker class serve several unrelated Originators. Its limitation is that it works best when T is already an immutable, self-contained value; for Originators whose true internal representation should stay hidden even from the Memento's own declared type parameter, a dedicated per-Originator nested Memento class still gives stronger encapsulation.
final class GenericMemento<T> {
private final T state;
GenericMemento(T state) { this.state = state; }
T getState() { return state; } // acceptable if T is an immutable value type
}
67. Discuss the pitfalls of serialVersionUID management for a Serializable Memento class whose Originator's fields evolve across application versions.
If a class implementing Serializable does not declare an explicit serialVersionUID, the JVM computes one automatically from the class's structure, so any change, even an unrelated method addition, can shift that computed value and make previously serialized mementos fail to deserialize with an InvalidClassException. Always declare serialVersionUID explicitly and only bump it deliberately, alongside a documented migration story for old snapshots, when you make an incompatible field change.
private static final class DocumentSnapshot implements java.io.Serializable {
private static final long serialVersionUID = 1L; // bump deliberately, not automatically
private final String text;
DocumentSnapshot(String text) { this.text = text; }
}
68. Design an undo mechanism for edits to a tree-shaped document, such as an AST or a JSON document editor, using the Memento pattern.
For small, localized edits (renaming one node, changing one value) a full-tree snapshot on every change is wasteful; instead, snapshot only the smallest subtree that actually changed, plus a reference to where it was reattached in the parent tree, so restore only needs to swap that subtree back in rather than replacing the entire document.
record SubtreeMemento(String parentPath, JsonNode previousSubtree) {}
void restore(SubtreeMemento m, JsonDocument doc) {
doc.replaceAt(m.parentPath(), m.previousSubtree());
}
69. How would you use the Memento pattern to preserve an e-commerce shopping cart's state across a multi-step checkout wizard, so a user who backs out midway does not lose their cart?
Snapshot the cart (items, quantities, applied discount codes) at the moment checkout begins; if the user abandons checkout or an error occurs partway through payment, restore the pre-checkout memento so the cart returns to exactly its prior state rather than reflecting any partial, possibly inconsistent, checkout-time mutations (such as a discount that was tentatively applied and then should have been reverted).
Cart.Memento beforeCheckout = cart.snapshot();
try {
checkoutFlow.run(cart);
} catch (CheckoutFailedException e) {
cart.restore(beforeCheckout);
}
70. What are the risks of overusing the Memento pattern, applying it even where a much simpler solution, such as re-fetching data or recomputing from an immutable source, would work just as well?
Memento adds a class (or several) and a Caretaker layer for storage, which is unnecessary overhead if the "previous state" is trivially cheap to recompute from an authoritative source, for example re-fetching a record from the database rather than snapshotting it in memory first. Overuse also shows up when developers apply Memento to state that is already immutable by construction, where there is nothing to snapshot because nothing can change in place to begin with.
71. Design an edit-history feature for a spreadsheet application's individual cells using the Memento pattern, so a user can undo a formula or value change to one specific cell.
Each cell edit captures a small memento containing just that cell's previous raw value or formula string (not the whole spreadsheet), pushed onto a global undo stack alongside the cell's coordinates; because changing one cell can trigger recalculation of dependent cells, the memento must also decide whether to snapshot those dependents' computed values or simply re-trigger recalculation after restoring the edited cell.
record CellEditMemento(String cellRef, String previousFormula) {}
void undo(CellEditMemento m, Spreadsheet sheet) {
sheet.setFormula(m.cellRef(), m.previousFormula());
sheet.recalculateDependents(m.cellRef());
}
72. How would you implement a mobile app's "draft" auto-save feature, preserving an unsent message or unposted form as a restorable draft, using the Memento pattern?
Periodically (and on app backgrounding) capture a memento of the in-progress input and persist it locally, keyed by which screen or compose session it belongs to; on returning to that screen, check for a saved draft memento and restore it before the user starts typing again, then clear the persisted draft once the message is actually sent or explicitly discarded.
void onAppBackgrounded(ComposeScreen screen) {
localStore.save("draft:" + screen.id(), screen.snapshot());
}
void onScreenOpened(ComposeScreen screen) {
localStore.load("draft:" + screen.id()).ifPresent(screen::restore);
}
73. Discuss combining the Memento pattern with the Iterator pattern to let a Caretaker's stored history be walked (for display in a history panel) without exposing the mutable underlying collection.
Rather than exposing the Caretaker's internal Deque or List directly (which would let a UI component accidentally mutate the history), expose only an Iterator (or a read-only Iterable view) over the stored mementos' metadata (timestamp, label), so a history panel can render entries without any risk of reordering or removing them out from under the Caretaker.
Iterable<HistoryEntryView> visibleHistory() {
return () -> entries.stream().map(HistoryEntryView::from).iterator();
}
74. Discuss using an embedded in-memory database transaction (such as H2 in embedded mode) as the Caretaker for an application whose Originator's state is naturally table-shaped.
If the Originator's state already lives in an in-memory relational store for query convenience, an application-level "undo" can be implemented by wrapping each user action in its own database transaction and rolling that transaction back on undo, effectively letting the database engine itself act as the Caretaker rather than hand-rolling an object-level snapshot mechanism.
conn.setAutoCommit(false);
Savepoint sp = conn.setSavepoint();
applyUserAction(conn, action);
// on undo:
conn.rollback(sp);
This works well when all relevant state truly lives in that store, but breaks down the moment some of the Originator's state lives outside the database, such as an in-memory cache that the rollback does not touch.
75. Explain how to revert a shopping cart or order to its pre-transaction state after a failed checkout, ensuring inventory reservations and applied promotions are also correctly rolled back.
A checkout that reserves inventory and applies a promotion code before payment actually succeeds must treat those as part of the same rollback unit as the cart itself; a naive Memento that only restores the cart's line items while leaving inventory reserved, or a promotion marked as used, leaves the system in an inconsistent state even though the "undo" from the user's point of view looks complete.
CheckoutMemento before = new CheckoutMemento(cart.snapshot(), inventory.snapshot(), promotions.snapshot());
try {
processPayment(order);
} catch (PaymentFailedException e) {
before.restoreAll(); // cart, inventory reservation, and promotion usage all rolled back together
}
76. Discuss how to handle an exception thrown partway through restore(memento), where some fields have already been written before the failure, risking a partially restored, inconsistent Originator.
Build the fully restored state into a temporary local object first, validating and constructing everything that could fail before touching the Originator's actual fields at all, then assign the Originator's fields only once every part of the restore has succeeded; this way, if construction of the restored state throws partway through, the live Originator's fields are never touched and remain in their pre-restore state rather than a half-updated one.
void restore(Memento m) {
OrderState fullyBuilt = OrderState.rebuildFrom(m); // can throw; nothing mutated yet
this.state = fullyBuilt; // single atomic assignment, only reached on success
}
77. Explain how the Memento pattern can be used in test setup and teardown to snapshot and restore shared test fixture state between test methods, avoiding expensive re-initialization.
When a shared, expensive-to-build fixture (a populated in-memory cache, a configured object graph) needs to start each test in an identical state, capture a memento of the freshly built fixture once, then restore from that memento before every test method instead of rebuilding the fixture from scratch each time, trading a cheap restore for an expensive reconstruction.
@BeforeAll
static void buildFixtureOnce() { fixture = buildExpensiveFixture(); initialSnapshot = fixture.snapshot(); }
@BeforeEach
void resetFixture() { fixture.restore(initialSnapshot); }
78. Discuss combining the Memento pattern with the Visitor pattern to support exporting a snapshot to multiple external file formats, such as JSON and XML, without the Memento class itself knowing about either format.
Rather than adding a toJson() and a toXml() method directly onto the Memento class, coupling it to every export format ever needed, define a MementoExportVisitor interface with one method per format, and have the Memento accept a visitor and pass its (privately held) fields to it; new export formats are then added as new visitor implementations without touching the Memento class at all.
interface MementoExportVisitor { void visitDocumentMemento(String text, int cursor); }
class Memento {
private final String text; private final int cursor;
void accept(MementoExportVisitor visitor) { visitor.visitDocumentMemento(text, cursor); }
}
79. Discuss encrypting mementos before persisting them to disk or sending them over the network, and where the encryption step should live in the architecture.
Encryption should happen at the boundary where the memento leaves trusted in-process memory, typically in the persistence or transport adapter, not inside the Memento class itself, which should stay a plain data holder unaware of storage concerns. A common approach is to serialize the memento to bytes, encrypt those bytes with a key managed by a dedicated secrets component, and only ever write the encrypted form to disk or across the wire.
byte[] plainBytes = SerializingMementoFactory.capture(doc.snapshot());
byte[] encrypted = encryptionService.encrypt(plainBytes);
java.nio.file.Files.write(saveFilePath(), encrypted);
80. Discuss compression strategies for storing a long history of mementos efficiently, such as compressing individual snapshots or delta-compressing the sequence of them.
For large, mostly-similar consecutive mementos (such as full-document text snapshots), a general-purpose compressor like GZIP can shrink each snapshot considerably since much of the content repeats between adjacent versions; delta-compression goes further by storing only the difference between consecutive snapshots (similar to the diff-based memento technique) and periodically inserting a full "key" snapshot so restoring an arbitrary point does not require replaying the entire history from the beginning.
byte[] compressed;
try (var bos = new java.io.ByteArrayOutputStream();
var gzip = new java.util.zip.GZIPOutputStream(bos)) {
gzip.write(rawSnapshotBytes);
gzip.finish();
compressed = bos.toByteArray();
}
81. Design an undo mechanism for a rules engine or workflow engine, where undoing a step must also correctly restore the state of any pending timers or scheduled follow-up actions triggered by that step.
If executing a workflow step schedules a follow-up action (a reminder timer, a delayed retry), the memento captured before that step must also record enough information to cancel or restore that scheduled side effect on undo, not just the in-memory workflow state; otherwise undo can leave a "ghost" timer firing later against state that no longer reflects the step it was originally scheduled for.
record WorkflowStepMemento(WorkflowState stateBefore, java.util.List<String> scheduledTimerIdsToCancel) {}
void undo(WorkflowStepMemento m, WorkflowEngine engine) {
m.scheduledTimerIdsToCancel().forEach(engine::cancelTimer);
engine.restoreState(m.stateBefore());
}
82. Compare application-level Memento snapshotting to OS-level or filesystem-level snapshotting features, such as a "Time Machine"-style backup, in terms of scope and granularity.
OS or filesystem snapshots capture the entire state of a disk or volume at a point in time, coarse-grained and application-agnostic; they are excellent for disaster recovery but have no concept of "undo the last edit to this one document" within a running application, and restoring one typically requires reverting far more than just the object a user actually wants to roll back. An application-level Memento is fine-grained and semantically aware, restoring exactly the piece of state the application's own domain logic considers relevant, at the cost of needing to be deliberately implemented by the application itself rather than provided for free by the platform.
83. Describe a round-trip test strategy that asserts capturing a memento and immediately restoring it produces state that is bit-for-bit, field-for-field identical to the original.
Capture a memento without mutating the Originator at all in between, restore it back into the same Originator instance, and assert deep equality against a separately captured "expected" copy taken before the round trip; this specifically isolates bugs in the capture/restore machinery itself from bugs caused by intervening mutation, which a normal "mutate then undo" test does not fully separate.
@Test
void roundTripProducesIdenticalState() {
Document doc = new Document();
doc.type("round trip test");
Document expected = doc.deepCopyForTestComparison();
Document.Memento m = doc.snapshot();
doc.restore(m);
assertThat(doc).usingRecursiveComparison().isEqualTo(expected);
}
84. Explain why createMemento() should defensively copy any mutable field at the moment of capture, even if the Originator's own mutating methods look safe at first glance, to guard against concurrent modification during capture.
Even a single-threaded Originator can hand out a live reference to one of its mutable fields elsewhere in the codebase (say, into a UI adapter for rendering); if that external code later mutates the object through that reference, a memento that stored the same reference rather than a copy would silently reflect the external mutation too, even though the memento was supposedly a snapshot "frozen in time."
Memento save() {
// defensive copy at capture time, regardless of how "safe" other code looks today
return new Memento(new java.util.ArrayList<>(this.items));
}
85. Design a rollback mechanism for a distributed saga, where a multi-step business transaction spanning several services must be reversed if a later step fails, using Memento-like compensation.
Because a saga cannot use a single ACID transaction across services, each step instead records enough information, a compensation memento, to define an explicit compensating action (refund a payment, release an inventory reservation) rather than a literal state restore; if a later step fails, the saga orchestrator walks backward through completed steps' compensation mementos, invoking each one's compensating action in reverse order.
record SagaStepCompensation(String stepName, Runnable compensate) {}
void rollbackSaga(java.util.Deque<SagaStepCompensation> completedSteps) {
while (!completedSteps.isEmpty()) completedSteps.pop().compensate().run();
}
86. Discuss choosing a concurrent, thread-safe data structure, such as ConcurrentLinkedDeque, for a Caretaker that must accept mementos pushed from multiple threads safely.
If several threads can each independently trigger a snapshot (for example, multiple background workers each mutating a shared, thread-safe Originator), a plain ArrayDeque Caretaker is not safe for concurrent pushes and pops; ConcurrentLinkedDeque supports safe concurrent access without external synchronization, though ordering guarantees between racing threads still need to be considered separately based on what "undo order" should mean in that scenario.
java.util.concurrent.ConcurrentLinkedDeque<Document.Memento> history = new java.util.concurrent.ConcurrentLinkedDeque<>();
history.push(doc.snapshot()); // safe under concurrent access from multiple threads
87. Discuss implementing a "soft delete" or trash/recycle-bin feature using the Memento pattern, where a deleted item can be restored later exactly as it was.
Rather than immediately and permanently removing an item, capture a memento of it at the moment of "deletion" and move that memento into a trash Caretaker, keeping it available for a defined retention period; "restore from trash" simply restores the memento back into the live collection, and a background job later purges mementos whose retention period has expired.
class TrashBin {
private final java.util.Map<String, ItemMemento> deletedItems = new java.util.HashMap<>();
void softDelete(String id, Item item) { deletedItems.put(id, item.snapshot()); }
void restoreFromTrash(String id, ItemRepository repo) { repo.restore(deletedItems.remove(id)); }
}
88. Explain why using the Memento pattern is preferable to a naive manual "backup object" hack that copies fields into a loosely-typed structure like a Map<String, Object>.
A manual field-into-a-map backup gives up compile-time type safety, since every field's type has been erased to Object, and it typically requires remembering, by convention rather than enforcement, exactly which keys correspond to which fields; a proper Memento keeps each field statically typed and lets the compiler catch a mismatch between what was captured and what restore expects. The Map-based approach also usually ends up publicly readable, since a bare Map has no way to hide its contents from the Caretaker, defeating the encapsulation the pattern exists to provide.
89. Clarify the common interview trap of using "memento" and generic "backup/restore" terminology interchangeably, and explain the precise technical distinction an interviewer expects.
"Backup and restore" is a broad, informal description of the goal; "Memento" is the specific design pattern for achieving that goal in an object-oriented codebase while preserving encapsulation, with three named participants and a specific narrow/wide interface split. An answer that only says "you save the state somewhere and load it back" without mentioning that the Caretaker cannot see the memento's internals is describing backup/restore generically, not demonstrating understanding of the actual GoF pattern.
90. Design an undo mechanism for a spreadsheet-like formula engine where undoing a cell's value change must also correctly re-trigger re-evaluation of a dependency graph of derived cells.
Because changing one cell can cascade through a dependency graph and update many derived cells' cached computed values, restoring a single cell's raw formula memento is not enough by itself; after restoring the formula, the engine must re-run its dependency-graph evaluation (typically a topological-order recalculation) so every downstream cell's cached value reflects the restored formula, not the value it had immediately before the undo.
void undoCellEdit(CellEditMemento m, FormulaEngine engine) {
engine.setRawFormula(m.cellRef(), m.previousFormula());
engine.recalculateDependencyGraphFrom(m.cellRef()); // cascades through derived cells
}
91. Walk through a real production incident caused by a mutable memento field, where "undo" appeared to succeed in the logs but the user-visible state never actually changed back.
An editor's memento stored a reference to the document's live StringBuilder rather than a copied String; every subsequent edit mutated that same StringBuilder in place, so by the time the user pressed undo, the "captured" memento's StringBuilder already contained the newest text, identical to what was currently displayed. The undo code executed without error and logged "restored previous state," but visually nothing changed, since the memento and the live document had been the same mutable object all along.
// BUG: StringBuilder is mutable and shared, not copied
static final class Memento { final StringBuilder text; Memento(StringBuilder text) { this.text = text; } }
// FIX: snapshot as an immutable String
static final class Memento { final String text; Memento(StringBuilder text) { this.text = text.toString(); } }
92. Discuss how you would document a Memento-based undo API for other developers on your team, so they correctly extend the Originator without accidentally breaking the encapsulation guarantee.
Document explicitly, at the top of the Originator class, which fields are included in the memento snapshot and which are deliberately excluded (and why, such as sensitive fields or derived/cacheable fields), and state clearly, in the Memento class's own Javadoc, that no accessor should ever be widened beyond package-private without a deliberate design discussion. It also helps to include one worked example test showing the intended capture/mutate/restore round trip, which doubles as both documentation and a regression test against future refactors.
93. Discuss combining Memento with Command's macro/composite command support, where a single user-visible "undo" must reverse a whole batch of smaller commands executed together, such as a find-and-replace-all operation.
A macro command executes several sub-commands as one logical unit and should present as a single entry in the undo history; internally, each sub-command still captures its own memento before running, and the macro's undo() replays those sub-command undos in reverse order, so partial replacements are consistently reversed together rather than leaving some replaced and others not.
class MacroCommand implements Command {
private final java.util.List<Command> subCommands;
MacroCommand(java.util.List<Command> subCommands) { this.subCommands = subCommands; }
@Override public void execute() { subCommands.forEach(Command::execute); }
@Override public void undo() {
for (int i = subCommands.size() - 1; i >= 0; i--) subCommands.get(i).undo(); // reverse order
}
}
94. Discuss how you would profile a production application's memory usage to determine whether its Memento-based undo stack is a significant contributor to overall heap consumption.
Take a heap dump under realistic usage and inspect the dominator tree (using a tool such as Eclipse MAT or VisualVM) for the Caretaker's collection, checking both the number of retained mementos and the average retained size per memento; comparing retained size before and after a long editing session with heavy undo usage isolates whether history growth, rather than some unrelated leak, explains rising memory usage over time.
95. Describe how you would migrate a legacy codebase's ad-hoc "manually copy fields into a backup object" hack into a proper Memento implementation, without a risky big-bang rewrite.
Start by introducing a proper, private nested Memento class alongside the existing backup object, and have the legacy backup code delegate to it internally, one field at a time, verifying via tests that the new path produces identical restore behavior to the old one before removing the old backup object entirely. This incremental strangler-style migration keeps the system working at every step and lets you fix any accidentally-shared mutable references you discover along the way, one at a time, rather than all at once under time pressure.
96. Design a rollback mechanism for a multi-step batch job that must be reverted partway through processing if a later record in the batch fails validation.
Before processing each record, capture a memento of any shared aggregate state the batch job mutates as it runs (a running total, an in-memory summary object); if a later record fails and the whole batch must be treated as all-or-nothing, restore the memento captured before the very first record, discarding every partial mutation the batch made up to the point of failure, rather than leaving the aggregate reflecting only some of the batch's records.
BatchSummary.Memento beforeBatch = summary.snapshot();
try {
for (Record r : records) process(r, summary);
} catch (ValidationException e) {
summary.restore(beforeBatch); // whole batch treated as a single unit
throw new BatchFailedException(e);
}
97. Compare storing every individual keystroke as its own memento versus storing coarser-grained "edit operations" (insert word, delete sentence) as the unit of undo, and how you would decide the right granularity.
Keystroke-level mementos give the most precise possible undo but bloat history size and rarely match what a user actually wants to reverse, since undoing "one keystroke" of a five-character word typed a second ago feels tedious. Coarser operation-level mementos, grouping a burst of related keystrokes into one semantic edit (typing a whole word, pasting a block), better match user intent and dramatically reduce the number of stored history entries, at the cost of slightly more complex logic to decide where one "operation" ends and the next begins.
The right granularity is whatever a user would naturally describe as "one thing I did," which for text is usually closer to a word or a pause in typing than a single character.
98. Design a full IDE-style undo/redo system that supports grouped, multi-file edits (such as a project-wide rename-symbol refactor) as a single undoable unit using Memento and Command together.
A project-wide rename touches many files, so it should be represented as one macro Command whose sub-commands each hold a per-file memento of that file's contents before the rename; the macro's single history entry, visible to the user as "Undo Rename Symbol," reverses every touched file's sub-command in reverse order when triggered, so a partial undo mid-refactor never leaves the project in a state where only some files were renamed back.
class RenameSymbolCommand implements Command {
private final java.util.List<FileEditCommand> perFileEdits;
RenameSymbolCommand(java.util.List<FileEditCommand> perFileEdits) { this.perFileEdits = perFileEdits; }
@Override public void execute() { perFileEdits.forEach(FileEditCommand::execute); }
@Override public void undo() {
for (int i = perFileEdits.size() - 1; i >= 0; i--) perFileEdits.get(i).undo();
}
}
99. Discuss adapting the Memento pattern for embedded or mobile systems with tightly constrained memory, where storing full-fidelity snapshots for a deep undo history is not affordable.
Under hard memory constraints, favor a shallow history depth (perhaps only the last one or two steps) over deep history, prefer delta-based mementos over full copies wherever the state supports it, and consider compressing or off-loading older mementos to flash storage rather than keeping every entry resident in RAM at once. It is also worth exposing the history depth as an explicit, tunable configuration value, since the right trade-off between "how far back can the user undo" and "how much memory can we spend on it" is a product decision, not purely a technical one.
100. Design a complete, reusable undo/redo library API from scratch in Java, built around the Memento pattern, that any Originator class in an application could plug into with minimal boilerplate.
Define a small, generic contract, an Originator<M> interface with M save() and void restore(M), and a reusable UndoRedoManager<M> Caretaker that works against any Originator implementing it, with a bounded history size, automatic redo-stack invalidation on new edits, and pluggable eviction and persistence strategies as optional extensions. Any class in the application then only needs to implement the two-method Originator contract and hand itself to the manager to get full undo/redo behavior, without duplicating stack-management logic per feature.
interface Originator<M> {
M save();
void restore(M memento);
}
class UndoRedoManager<M> {
private final int maxHistory;
private final java.util.Deque<M> undoStack = new java.util.ArrayDeque<>();
private final java.util.Deque<M> redoStack = new java.util.ArrayDeque<>();
UndoRedoManager(int maxHistory) { this.maxHistory = maxHistory; }
void beforeEdit(Originator<M> originator) {
undoStack.addFirst(originator.save());
if (undoStack.size() > maxHistory) undoStack.removeLast();
redoStack.clear();
}
void undo(Originator<M> originator) {
if (undoStack.isEmpty()) return;
redoStack.addFirst(originator.save());
originator.restore(undoStack.removeFirst());
}
void redo(Originator<M> originator) {
if (redoStack.isEmpty()) return;
undoStack.addFirst(originator.save());
originator.restore(redoStack.removeFirst());
}
boolean canUndo() { return !undoStack.isEmpty(); }
boolean canRedo() { return !redoStack.isEmpty(); }
}
Post a Comment
Add