Design Patterns (GoF and modern) Module 5 – Behavioral patterns: state and communication
Memento: snapshots that keep state private
Memento saves an object’s state in an opaque snapshot so it can be restored later: the originator, memento and caretaker roles, undo histories and save files.
What you will learn
- Implement save and restore through an opaque memento that only the originator can read
- Compare snapshot-based undo with command-based undo for memory and complexity
- Limit history size and handle large states with keyframes and deltas
- Save snapshots safely as versioned JSON instead of unsafe deserialisation
Before you start
What you’ll learn · Before you start
What you will learn
- Implement save and restore through an opaque memento that only the originator can read
- Compare snapshot-based undo with command-based undo for memory and complexity
- Limit history size and handle large states with keyframes and deltas
- Save snapshots safely as versioned JSON instead of unsafe deserialisation
Before you start
On this page
Press undo three times in a drawing app and the canvas must step back through exactly the three states it had, while the code that keeps those states should know nothing about how a canvas stores its shapes. Memento is the behavioural design pattern for that. The object being saved, the originator: In the Memento pattern, the object whose state is saved: it packs its state into a memento and is the only object that reads one again on restore., packs its state into a memento: An opaque snapshot of an object's state: the object that made it can restore itself from it, and anyone else can only store it and hand it back., and another object, the caretaker: In the Memento pattern, the object that keeps mementos, such as an undo history, and returns them on request without ever looking inside one., stores mementos and hands them back without ever looking inside one.
Undo is the classic use: an editor saves a memento before every change and restores the newest one when the user presses undo. Checkpoints in games, rolling back a half-finished form and the Cancel button of a settings dialog work the same way.
How Memento works
Suppose a drawing canvas keeps its shapes private, and the editor needs an undo history. If the history copied the canvas’s fields itself, it would depend on every one of them and break the day the canvas stored its shapes differently. With Memento the canvas makes the copy and the history only keeps it, so the canvas can change its insides at will:
Save before a change, restore on undo
Text description of the diagram
Three participants stand in columns: the History (the caretaker), the Canvas (the originator) and the Snapshot (the memento). Time runs from top to bottom.
- Before a change, the history calls save() on the canvas.
- The canvas packs its private state into a new snapshot.
- The canvas returns the snapshot to the history.
- The history keeps the snapshot and never reads what is inside it.
- On undo, the history passes the snapshot back with restore(snapshot).
- The canvas reads its own state back out of the snapshot.
Only the canvas ever reads a snapshot. The history knows how many snapshots it holds and in which order, nothing more.
A memento therefore has two faces. The caretaker sees a narrow one: an object it can store, count and hand back, and nothing else. The originator sees a wide one: everything it needs to rebuild its state. The snapshot is taken before each change, because the state before the change is exactly what undo must bring back.
The program below plays three edits and three undos. Its caretaker is a plain list, which can keep snapshots in order and return them, and nothing more. It runs in 14 languages with one output, and each tab’s note says how that language stops the list’s owner from reading a snapshot:
Python 3.14 · main.py
from collections.abc import Callable
from dataclasses import dataclass
@dataclass(frozen=True)
class Shape:
kind: str
x: int
y: int
class Canvas:
"""The originator: the only object that reads a snapshot."""
class Snapshot:
"""The memento: private by convention."""
__slots__ = ("_shapes",)
def __init__(self, shapes: tuple[Shape, ...]) -> None:
self._shapes = shapes
def __init__(self) -> None:
self._shapes: tuple[Shape, ...] = ()
def draw(self, kind: str, x: int, y: int) -> None:
self._shapes += (Shape(kind, x, y),)
def move_last(self, dx: int, dy: int) -> None:
last = self._shapes[-1]
self._shapes = (*self._shapes[:-1], Shape(last.kind, last.x + dx, last.y + dy))
def save(self) -> "Canvas.Snapshot":
return Canvas.Snapshot(self._shapes)
def restore(self, snapshot: "Canvas.Snapshot") -> None:
self._shapes = snapshot._shapes
def describe(self) -> str:
return ", ".join(f"{s.kind} at {s.x},{s.y}" for s in self._shapes) or "(empty)"
canvas = Canvas()
history: list[Canvas.Snapshot] = [] # the caretaker: it never looks inside a snapshot
edits: list[tuple[str, Callable[[], None]]] = [
("draw a circle", lambda: canvas.draw("circle", 10, 10)),
("draw a square", lambda: canvas.draw("square", 40, 10)),
("move the square", lambda: canvas.move_last(5, 5)),
]
for label, edit in edits:
history.append(canvas.save()) # save before every change
edit()
print(f"{label}: {canvas.describe()}")
print(f"the history holds {len(history)} snapshots")
while history:
canvas.restore(history.pop())
print(f"undo: {canvas.describe()}") In Python: Python has no private members, so the underscore of _shapes marks the snapshot's slot private by convention only. The shapes are frozen and kept in tuples, so a snapshot can share them with the canvas.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
/** What each snapshot stands for, read only by Canvas. */
const contents = new WeakMap();
/** The originator: the only object that reads a snapshot. */
class Canvas {
#shapes = [];
draw(kind, x, y) {
this.#shapes = [...this.#shapes, { kind, x, y }];
}
moveLast(dx, dy) {
const last = this.#shapes.at(-1);
this.#shapes = [...this.#shapes.slice(0, -1), { ...last, x: last.x + dx, y: last.y + dy }];
}
save() {
const snapshot = Object.freeze({}); // the memento: an empty token
contents.set(snapshot, this.#shapes);
return snapshot;
}
restore(snapshot) {
this.#shapes = contents.get(snapshot);
}
describe() {
return this.#shapes.map((s) => `${s.kind} at ${s.x},${s.y}`).join(", ") || "(empty)";
}
}
const canvas = new Canvas();
const history = []; // the caretaker: it never looks inside a snapshot
const edits = [
["draw a circle", () => canvas.draw("circle", 10, 10)],
["draw a square", () => canvas.draw("square", 40, 10)],
["move the square", () => canvas.moveLast(5, 5)],
];
for (const [label, edit] of edits) {
history.push(canvas.save()); // save before every change
edit();
console.log(`${label}: ${canvas.describe()}`);
}
console.log(`the history holds ${history.length} snapshots`);
while (history.length > 0) {
canvas.restore(history.pop());
console.log(`undo: ${canvas.describe()}`);
} In JavaScript: A snapshot is an empty frozen object, and a WeakMap of the module holds what it stands for, so code outside the module cannot read it, and the shapes go once the snapshot does.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
var canvas = new Canvas();
List<Canvas.Memento> history = new ArrayList<>(); // the caretaker: it never looks inside one
List<String> labels = List.of("draw a circle", "draw a square", "move the square");
List<Runnable> edits =
List.of(
() -> canvas.draw("circle", 10, 10),
() -> canvas.draw("square", 40, 10),
() -> canvas.moveLast(5, 5));
for (int i = 0; i < edits.size(); i++) {
history.add(canvas.save()); // save before every change
edits.get(i).run();
System.out.println(labels.get(i) + ": " + canvas.describe());
}
System.out.println("the history holds " + history.size() + " snapshots");
while (!history.isEmpty()) {
canvas.restore(history.remove(history.size() - 1));
System.out.println("undo: " + canvas.describe());
}
}
}
record Shape(String kind, int x, int y) {}
/** The originator: the only object that reads a snapshot. */
final class Canvas {
interface Memento {}
/** The memento. */
private record Snapshot(List<Shape> shapes) implements Memento {}
private final List<Shape> shapes = new ArrayList<>();
void draw(String kind, int x, int y) {
shapes.add(new Shape(kind, x, y));
}
void moveLast(int dx, int dy) {
Shape last = shapes.remove(shapes.size() - 1);
shapes.add(new Shape(last.kind(), last.x() + dx, last.y() + dy));
}
Memento save() {
return new Snapshot(List.copyOf(shapes)); // a copy, which later edits cannot reach
}
void restore(Memento memento) {
shapes.clear();
shapes.addAll(((Snapshot) memento).shapes());
}
String describe() {
if (shapes.isEmpty()) return "(empty)";
return shapes.stream()
.map(s -> s.kind() + " at " + s.x() + "," + s.y())
.collect(Collectors.joining(", "));
}
} In Java: Snapshot is a private record nested in Canvas, so main, in another top-level class, sees only the empty Memento interface. save() stores List.copyOf(shapes), a copy that later edits cannot reach.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
#include <format>
#include <print>
#include <string>
#include <utility>
#include <vector>
struct Shape {
std::string kind;
int x, y;
};
// The memento: only its friend Canvas makes or reads one.
class Snapshot {
friend class Canvas;
explicit Snapshot(std::vector<Shape> shapes) : shapes_(std::move(shapes)) {}
std::vector<Shape> shapes_;
};
// The originator: the only object that reads a snapshot.
class Canvas {
public:
void draw(std::string kind, int x, int y) {
shapes_.push_back({std::move(kind), x, y});
}
void move_last(int dx, int dy) {
shapes_.back().x += dx;
shapes_.back().y += dy;
}
Snapshot save() const { return Snapshot(shapes_); } // a copy of the shapes
void restore(Snapshot snapshot) { shapes_ = std::move(snapshot.shapes_); }
std::string describe() const {
std::string text;
for (const Shape &s : shapes_)
text += std::format("{}{} at {},{}", text.empty() ? "" : ", ", s.kind,
s.x, s.y);
return text.empty() ? "(empty)" : text;
}
private:
std::vector<Shape> shapes_;
};
int main() {
Canvas canvas;
std::vector<Snapshot>
history; // the caretaker: it never looks inside a snapshot
const std::pair<const char *, void (*)(Canvas &)> edits[] = {
{"draw a circle", [](Canvas &c) { c.draw("circle", 10, 10); }},
{"draw a square", [](Canvas &c) { c.draw("square", 40, 10); }},
{"move the square", [](Canvas &c) { c.move_last(5, 5); }}};
for (const auto &[label, edit] : edits) {
history.push_back(canvas.save()); // save before every change
edit(canvas);
std::println("{}: {}", label, canvas.describe());
}
std::println("the history holds {} snapshots", history.size());
while (!history.empty()) {
canvas.restore(std::move(history.back()));
history.pop_back();
std::println("undo: {}", canvas.describe());
}
} In C++: Every member of Snapshot is private, its constructor included, and friend class Canvas is the one exception, so other code can store and move snapshots but can neither make nor read one.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
var canvas = new Canvas();
var history = new Stack<IMemento>(); // the caretaker: it never looks inside a snapshot
(string Label, Action Edit)[] edits =
[
("draw a circle", () => canvas.Draw("circle", 10, 10)),
("draw a square", () => canvas.Draw("square", 40, 10)),
("move the square", () => canvas.MoveLast(5, 5)),
];
foreach (var (label, edit) in edits)
{
history.Push(canvas.Save()); // save before every change
edit();
Console.WriteLine($"{label}: {canvas.Describe()}");
}
Console.WriteLine($"the history holds {history.Count} snapshots");
while (history.Count > 0)
{
canvas.Restore(history.Pop());
Console.WriteLine($"undo: {canvas.Describe()}");
}
sealed record Shape(string Kind, int X, int Y);
interface IMemento
{
}
/// <summary>The originator: the only object that reads a snapshot.</summary>
sealed class Canvas
{
// The memento.
sealed record Snapshot(IReadOnlyList<Shape> Shapes) : IMemento;
IReadOnlyList<Shape> shapes = [];
public void Draw(string kind, int x, int y) => shapes = [.. shapes, new Shape(kind, x, y)];
public void MoveLast(int dx, int dy)
{
var last = shapes[^1];
shapes = [.. shapes.Take(shapes.Count - 1), last with { X = last.X + dx, Y = last.Y + dy }];
}
public IMemento Save() => new Snapshot(shapes);
public void Restore(IMemento memento) => shapes = ((Snapshot)memento).Shapes;
public string Describe() =>
shapes.Count == 0
? "(empty)"
: string.Join(", ", shapes.Select(s => $"{s.Kind} at {s.X},{s.Y}"));
} In C#: Snapshot is a private record nested in Canvas, so the top-level code holds IMemento references, an interface with nothing in it.
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
#include <stdio.h>
#include <stdlib.h>
typedef struct {
const char *kind;
int x, y;
} Shape;
// The originator: the only code that reads a snapshot.
typedef struct {
Shape shapes[8];
size_t count;
} Canvas;
// The memento: defined after main, so main cannot read one.
typedef struct Snapshot Snapshot;
static Snapshot *save(const Canvas *canvas);
static void restore(Canvas *canvas, Snapshot *snapshot);
static void draw(Canvas *canvas, const char *kind, int x, int y) {
canvas->shapes[canvas->count++] = (Shape){kind, x, y};
}
static void move_last(Canvas *canvas, int dx, int dy) {
canvas->shapes[canvas->count - 1].x += dx;
canvas->shapes[canvas->count - 1].y += dy;
}
static void describe(const char *label, const Canvas *canvas) {
printf("%s: ", label);
for (size_t i = 0; i < canvas->count; i++) {
const Shape *shape = &canvas->shapes[i];
printf("%s%s at %d,%d", i > 0 ? ", " : "", shape->kind, shape->x, shape->y);
}
puts(canvas->count == 0 ? "(empty)" : "");
}
static void draw_circle(Canvas *canvas) { draw(canvas, "circle", 10, 10); }
static void draw_square(Canvas *canvas) { draw(canvas, "square", 40, 10); }
static void move_square(Canvas *canvas) { move_last(canvas, 5, 5); }
int main(void) {
Canvas canvas = {.count = 0};
Snapshot *history[8]; // the caretaker: it never looks inside a snapshot
size_t saved = 0;
const struct {
const char *label;
void (*apply)(Canvas *canvas);
} edits[] = {{"draw a circle", draw_circle},
{"draw a square", draw_square},
{"move the square", move_square}};
for (size_t i = 0; i < sizeof edits / sizeof edits[0]; i++) {
history[saved++] = save(&canvas); // save before every change
edits[i].apply(&canvas);
describe(edits[i].label, &canvas);
}
printf("the history holds %zu snapshots\n", saved);
while (saved > 0) {
restore(&canvas, history[--saved]);
describe("undo", &canvas);
}
}
struct Snapshot {
Canvas state;
};
static Snapshot *save(const Canvas *canvas) {
Snapshot *snapshot = malloc(sizeof *snapshot);
if (snapshot == nullptr) {
abort();
}
snapshot->state = *canvas; // a copy, which later edits cannot reach
return snapshot;
}
static void restore(Canvas *canvas, Snapshot *snapshot) {
*canvas = snapshot->state;
free(snapshot); // restoring uses the snapshot up
} In C: Snapshot is declared before main and defined after it, so main can store and pass snapshot pointers while the compiler refuses any read of their fields; a header and a separate .c file give the same guarantee across files. restore() frees the snapshot it uses.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
final readonly class Shape
{
public function __construct(public string $kind, public int $x, public int $y) {}
}
/** The memento: an empty token. */
final class Snapshot {}
/** The originator: the only object that reads a snapshot. */
final class Canvas
{
/** @var list<Shape> */
private array $shapes = [];
/** @param WeakMap<Snapshot, list<Shape>> $contents what each snapshot stands for */
public function __construct(private WeakMap $contents = new WeakMap()) {}
public function draw(string $kind, int $x, int $y): void
{
$this->shapes[] = new Shape($kind, $x, $y);
}
public function moveLast(int $dx, int $dy): void
{
$last = array_pop($this->shapes);
$this->shapes[] = new Shape($last->kind, $last->x + $dx, $last->y + $dy);
}
public function save(): Snapshot
{
$snapshot = new Snapshot();
$this->contents[$snapshot] = $this->shapes; // arrays are values, so this is a copy
return $snapshot;
}
public function restore(Snapshot $snapshot): void
{
$this->shapes = $this->contents[$snapshot];
}
public function describe(): string
{
$parts = array_map(fn(Shape $s) => "{$s->kind} at {$s->x},{$s->y}", $this->shapes);
return $parts === [] ? '(empty)' : implode(', ', $parts);
}
}
$canvas = new Canvas();
$history = []; // the caretaker: it never looks inside a snapshot
$edits = [
'draw a circle' => fn() => $canvas->draw('circle', 10, 10),
'draw a square' => fn() => $canvas->draw('square', 40, 10),
'move the square' => fn() => $canvas->moveLast(5, 5),
];
foreach ($edits as $label => $edit) {
$history[] = $canvas->save(); // save before every change
$edit();
echo "{$label}: {$canvas->describe()}\n";
}
echo 'the history holds ' . count($history) . " snapshots\n";
while ($history !== []) {
$canvas->restore(array_pop($history));
echo "undo: {$canvas->describe()}\n";
} In PHP: A snapshot is an empty object, and Canvas keeps what it stands for in a WeakMap, a private property, so nothing outside can reach the shapes, which vanish with the snapshot.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
mod canvas {
#[derive(Clone)]
pub struct Shape {
kind: &'static str,
x: i32,
y: i32,
}
/// The memento.
pub struct Snapshot {
shapes: Vec<Shape>,
}
/// The originator: the only object that reads a snapshot.
#[derive(Default)]
pub struct Canvas {
shapes: Vec<Shape>,
}
impl Canvas {
pub fn draw(&mut self, kind: &'static str, x: i32, y: i32) {
self.shapes.push(Shape { kind, x, y });
}
pub fn move_last(&mut self, dx: i32, dy: i32) {
if let Some(last) = self.shapes.last_mut() {
last.x += dx;
last.y += dy;
}
}
pub fn save(&self) -> Snapshot {
Snapshot {
shapes: self.shapes.clone(),
}
}
pub fn restore(&mut self, snapshot: Snapshot) {
self.shapes = snapshot.shapes;
}
pub fn describe(&self) -> String {
let parts: Vec<String> = self
.shapes
.iter()
.map(|s| format!("{} at {},{}", s.kind, s.x, s.y))
.collect();
if parts.is_empty() {
"(empty)".to_string()
} else {
parts.join(", ")
}
}
}
}
use canvas::{Canvas, Snapshot};
fn main() {
let mut canvas = Canvas::default();
// The caretaker: it never looks inside a snapshot.
let mut history: Vec<Snapshot> = Vec::new();
let edits: [(&str, fn(&mut Canvas)); 3] = [
("draw a circle", |c| c.draw("circle", 10, 10)),
("draw a square", |c| c.draw("square", 40, 10)),
("move the square", |c| c.move_last(5, 5)),
];
for (label, edit) in edits {
history.push(canvas.save()); // save before every change
edit(&mut canvas);
println!("{label}: {}", canvas.describe());
}
println!("the history holds {} snapshots", history.len());
while let Some(snapshot) = history.pop() {
canvas.restore(snapshot);
println!("undo: {}", canvas.describe());
}
} In Rust: Snapshot's field is private to the canvas module, so main can store and move snapshots but the compiler refuses any code outside that reads one. restore takes the snapshot by value and uses it up.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
interface Shape {
readonly kind: string;
readonly x: number;
readonly y: number;
}
type Snapshot = { readonly __brand: "Snapshot" }; // the memento: an empty token
/** What each snapshot stands for, read only by Canvas. */
const contents = new WeakMap<Snapshot, readonly Shape[]>();
/** The originator: the only object that reads a snapshot. */
class Canvas {
#shapes: readonly Shape[] = [];
draw(kind: string, x: number, y: number): void {
this.#shapes = [...this.#shapes, { kind, x, y }];
}
moveLast(dx: number, dy: number): void {
const last = this.#shapes.at(-1);
if (last === undefined) return;
this.#shapes = [...this.#shapes.slice(0, -1), { ...last, x: last.x + dx, y: last.y + dy }];
}
save(): Snapshot {
const snapshot = Object.freeze({}) as Snapshot;
contents.set(snapshot, this.#shapes);
return snapshot;
}
restore(snapshot: Snapshot): void {
this.#shapes = contents.get(snapshot) ?? [];
}
describe(): string {
return this.#shapes.map((s) => `${s.kind} at ${s.x},${s.y}`).join(", ") || "(empty)";
}
}
const canvas = new Canvas();
const history: Snapshot[] = []; // the caretaker: it never looks inside a snapshot
const edits: [string, () => void][] = [
["draw a circle", () => canvas.draw("circle", 10, 10)],
["draw a square", () => canvas.draw("square", 40, 10)],
["move the square", () => canvas.moveLast(5, 5)],
];
for (const [label, edit] of edits) {
history.push(canvas.save()); // save before every change
edit();
console.log(`${label}: ${canvas.describe()}`);
}
console.log(`the history holds ${history.length} snapshots`);
for (let snapshot = history.pop(); snapshot !== undefined; snapshot = history.pop()) {
canvas.restore(snapshot);
console.log(`undo: ${canvas.describe()}`);
} In TypeScript: The same WeakMap token as in JavaScript. The branded type Snapshot lets callers store snapshots, but no property of one tells them anything.
Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
package main
import (
"fmt"
"slices"
"strings"
)
type Shape struct {
Kind string
X, Y int
}
// Snapshot is the memento.
type Snapshot struct{ shapes []Shape }
// Canvas is the originator: the only object that reads a snapshot.
type Canvas struct{ shapes []Shape }
func (c *Canvas) Draw(kind string, x, y int) { c.shapes = append(c.shapes, Shape{kind, x, y}) }
func (c *Canvas) MoveLast(dx, dy int) {
last := &c.shapes[len(c.shapes)-1]
last.X += dx
last.Y += dy
}
// Save and Restore copy the slice, which MoveLast changes in place.
func (c *Canvas) Save() Snapshot { return Snapshot{slices.Clone(c.shapes)} }
func (c *Canvas) Restore(s Snapshot) { c.shapes = slices.Clone(s.shapes) }
func (c *Canvas) Describe() string {
parts := make([]string, len(c.shapes))
for i, s := range c.shapes {
parts[i] = fmt.Sprintf("%s at %d,%d", s.Kind, s.X, s.Y)
}
if len(parts) == 0 {
return "(empty)"
}
return strings.Join(parts, ", ")
}
func main() {
canvas := &Canvas{}
var history []Snapshot // the caretaker: it never looks inside a snapshot
edits := []struct {
label string
edit func()
}{
{"draw a circle", func() { canvas.Draw("circle", 10, 10) }},
{"draw a square", func() { canvas.Draw("square", 40, 10) }},
{"move the square", func() { canvas.MoveLast(5, 5) }},
}
for _, e := range edits {
history = append(history, canvas.Save()) // save before every change
e.edit()
fmt.Printf("%s: %s\n", e.label, canvas.Describe())
}
fmt.Printf("the history holds %d snapshots\n", len(history))
for len(history) > 0 {
canvas.Restore(history[len(history)-1])
history = history[:len(history)-1]
fmt.Println("undo:", canvas.Describe())
}
} In Go: Go hides unexported names per package, so in another package Snapshot's field would be unreadable; this one-file program can only state that. Save and Restore copy the slice, because MoveLast changes it in place.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
struct Shape {
let kind: String
var x: Int
var y: Int
}
/// The originator: the only object that reads a snapshot.
final class Canvas {
/// The memento.
struct Snapshot {
fileprivate let shapes: [Shape]
}
private var shapes: [Shape] = []
func draw(_ kind: String, x: Int, y: Int) {
shapes.append(Shape(kind: kind, x: x, y: y))
}
func moveLast(dx: Int, dy: Int) {
shapes[shapes.count - 1].x += dx
shapes[shapes.count - 1].y += dy
}
func save() -> Snapshot { Snapshot(shapes: shapes) } // arrays are values: this is a copy
func restore(_ snapshot: Snapshot) { shapes = snapshot.shapes }
func describe() -> String {
shapes.isEmpty
? "(empty)" : shapes.map { "\($0.kind) at \($0.x),\($0.y)" }.joined(separator: ", ")
}
}
let canvas = Canvas()
var history: [Canvas.Snapshot] = [] // the caretaker: it never looks inside a snapshot
let edits: [(String, () -> Void)] = [
("draw a circle", { canvas.draw("circle", x: 10, y: 10) }),
("draw a square", { canvas.draw("square", x: 40, y: 10) }),
("move the square", { canvas.moveLast(dx: 5, dy: 5) }),
]
for (label, edit) in edits {
history.append(canvas.save()) // save before every change
edit()
print("\(label): \(canvas.describe())")
}
print("the history holds \(history.count) snapshots")
while let snapshot = history.popLast() {
canvas.restore(snapshot)
print("undo: \(canvas.describe())")
} In Swift: Access control works per file and module, so fileprivate hides the shapes from other files only; in this one file the opacity is stated, not enforced. Arrays are values, so a snapshot is a copy.
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
Shape = Data.define(:kind, :x, :y)
# The originator: the only object that reads a snapshot.
class Canvas
# The memento: a private constant.
Snapshot = Data.define(:shapes)
private_constant :Snapshot
def initialize
@shapes = [].freeze
end
def draw(kind, x_pos, y_pos)
@shapes = [*@shapes, Shape.new(kind, x_pos, y_pos)].freeze
end
def move_last(delta_x, delta_y)
last = @shapes.last
@shapes = [*@shapes[...-1], last.with(x: last.x + delta_x, y: last.y + delta_y)].freeze
end
def save = Snapshot.new(@shapes)
def restore(snapshot)
@shapes = snapshot.shapes
end
def describe
return '(empty)' if @shapes.empty?
@shapes.map { |shape| "#{shape.kind} at #{shape.x},#{shape.y}" }.join(', ')
end
end
canvas = Canvas.new
history = [] # the caretaker: it never looks inside a snapshot
edits = {
'draw a circle' => -> { canvas.draw('circle', 10, 10) },
'draw a square' => -> { canvas.draw('square', 40, 10) },
'move the square' => -> { canvas.move_last(5, 5) }
}
edits.each do |label, edit|
history.push(canvas.save) # save before every change
edit.call
puts "#{label}: #{canvas.describe}"
end
puts "the history holds #{history.size} snapshots"
until history.empty?
canvas.restore(history.pop)
puts "undo: #{canvas.describe}"
end In Ruby: private_constant hides the name Canvas::Snapshot outside the class, but any holder of a snapshot could still call shapes on it, so the opacity is a convention, as in Python.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
data class Shape(
val kind: String,
val x: Int,
val y: Int,
)
interface Memento
/** The originator: the only object that reads a snapshot. */
class Canvas {
/** The memento. */
private class Snapshot(
val shapes: List<Shape>,
) : Memento
private var shapes = listOf<Shape>() // read-only, so a snapshot can share it
fun draw(
kind: String,
x: Int,
y: Int,
) {
shapes = shapes + Shape(kind, x, y)
}
fun moveLast(
dx: Int,
dy: Int,
) {
val last = shapes.last()
shapes = shapes.dropLast(1) + last.copy(x = last.x + dx, y = last.y + dy)
}
fun save(): Memento = Snapshot(shapes)
fun restore(memento: Memento) {
shapes = (memento as Snapshot).shapes
}
fun describe(): String {
val text = shapes.joinToString(", ") { "${it.kind} at ${it.x},${it.y}" }
return text.ifEmpty { "(empty)" }
}
}
fun main() {
val canvas = Canvas()
val history = ArrayDeque<Memento>() // the caretaker: it never looks inside a snapshot
val edits =
listOf(
"draw a circle" to { canvas.draw("circle", 10, 10) },
"draw a square" to { canvas.draw("square", 40, 10) },
"move the square" to { canvas.moveLast(5, 5) },
)
for ((label, edit) in edits) {
history.addLast(canvas.save()) // save before every change
edit()
println("$label: ${canvas.describe()}")
}
println("the history holds ${history.size} snapshots")
while (history.isNotEmpty()) {
canvas.restore(history.removeLast())
println("undo: ${canvas.describe()}")
}
} In Kotlin: Snapshot is a private nested class behind the public Memento interface, so code outside Canvas can neither name the class nor read its shapes.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
typedef Shape = ({String kind, int x, int y});
/// The memento.
final class Snapshot {
Snapshot._(this._shapes);
final List<Shape> _shapes;
}
/// The originator: the only object that reads a snapshot.
final class Canvas {
List<Shape> _shapes = const [];
void draw(String kind, int x, int y) {
_shapes = List.unmodifiable([..._shapes, (kind: kind, x: x, y: y)]);
}
void moveLast(int dx, int dy) {
final last = _shapes.last;
final moved = (kind: last.kind, x: last.x + dx, y: last.y + dy);
_shapes = List.unmodifiable([..._shapes.take(_shapes.length - 1), moved]);
}
Snapshot save() => Snapshot._(_shapes);
void restore(Snapshot snapshot) => _shapes = snapshot._shapes;
String describe() => _shapes.isEmpty
? '(empty)'
: _shapes.map((s) => '${s.kind} at ${s.x},${s.y}').join(', ');
}
void main() {
final canvas = Canvas();
final history =
<Snapshot>[]; // the caretaker: it never looks inside a snapshot
final edits = <(String, void Function())>[
('draw a circle', () => canvas.draw('circle', 10, 10)),
('draw a square', () => canvas.draw('square', 40, 10)),
('move the square', () => canvas.moveLast(5, 5)),
];
for (final (label, edit) in edits) {
history.add(canvas.save()); // save before every change
edit();
print('$label: ${canvas.describe()}');
}
print('the history holds ${history.length} snapshots');
while (history.isNotEmpty) {
canvas.restore(history.removeLast());
print('undo: ${canvas.describe()}');
}
} In Dart: Privacy is per library, that is per file, so _shapes is hidden from other libraries. Snapshot._ is a private constructor, so code in other libraries cannot make a snapshot either.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
draw a circle: circle at 10,10 draw a square: circle at 10,10, square at 40,10 move the square: circle at 10,10, square at 45,15 the history holds 3 snapshots undo: circle at 10,10, square at 40,10 undo: circle at 10,10 undo: (empty)
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 13 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 153 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 7 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 12 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 20 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 722 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 188 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 15 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
A real undo history has more decisions to make, and each has a name:
- Snapshot-based undo stores a memento of the whole state before every change.
- Command-based undo stores each change together with a way to reverse it, instead of the state.
- Immutable state makes snapshots cheap, because a new snapshot can share every part that did not change.
- A bounded history keeps only the newest mementos and forgets the oldest.
- Keyframes with deltas store a full memento now and then, and only the changes in between.
- A save file is a memento written to disk, and loading one brings its own security rules.
Checkpoint
Check: The originator, the memento and the caretaker
Each language keeps the memento opaque in its own way Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Snapshots or commands: undo memory, measured
- Bounding the history: a cap, keyframes and deltas
- Saving snapshots without running code
- When not to use Memento
- Practise: undo for a sign-up wizard
- Undo for a sign-up wizard, with a capped history and JSON saves
- Interview questions
- A photo editor keeps a full copy of the image as a memento before every edit. Users editing 4,000 × 3,000 photos run out of memory after about 20 edits. Estimate what the history costs, and redesign it so that 100 undo steps fit in a fixed budget.
- A game writes its save files with Python's pickle, and players have started sharing save files online. What is the risk, and how do you move to a safe format without breaking the saves players already have?
- You are designing undo for a collaborative text editor whose documents reach 10 MB. Do you use mementos or commands? Defend the choice, including what undo should do when another user's edit lands between your edit and your undo.
- Key takeaways
1 diagram · 3 runnable examples (Python) · 1 exercise with sample tests · 3 interview answers · about 31 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- caretaker
- In the Memento pattern, the object that keeps mementos, such as an undo history, and returns them on request without ever looking inside one.
- memento
- An opaque snapshot of an object's state: the object that made it can restore itself from it, and anyone else can only store it and hand it back.
- originator
- In the Memento pattern, the object whose state is saved: it packs its state into a memento and is the only object that reads one again on restore.
References
- Design Patterns: Elements of Reusable Object-Oriented Software (Memento) (Addison-Wesley (Pearson))
- pickle: Python object serialization (Python 3.14) (Python Software Foundation)
- ObjectInputStream (Java SE 25): the warning on untrusted data (Oracle)
- dataclasses: frozen instances (Python 3.14) (Python Software Foundation)
- tracemalloc: trace memory allocations (Python 3.14) (Python Software Foundation)
- JEP 395: Records (OpenJDK)
- The Java Language Specification, Java SE 25: 6.6.1 Determining accessibility (Oracle)
- The Rust Programming Language: controlling visibility with pub (The Rust Project)
- C++ working draft: friends ([class.friend]) (ISO C++ committee (working draft))
- WeakMap (JavaScript reference) (MDN Web Docs)
- Access control (The Swift Programming Language) (Swift.org)
- Struct declaration (C reference): forward declarations and incomplete types (cppreference.com)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress