# Memento

> Capture an object's internal state in an opaque snapshot so it can be restored later without exposing that state to the code holding the snapshot.

- kind: pattern
- category: behavioral
- aliases: Snapshot, Token
- canonical: https://aipatterns.dev/design-patterns/memento
- inspired-by: https://refactoring.guru/design-patterns/memento

## Intent

Let an object save its own state into a snapshot object and later restore from it. Other code can store and pass the snapshot around, but can't read or change what is inside. Rollback and undo work without breaking the object's encapsulation.

## Forces

- **Rolling back** vs **keeping fields private**: the code that decides to roll back should not need to know which fields to reset.
- **Trying a change** vs **predicting its effect**: sometimes the only way to know whether a change is valid is to apply it and check.
- **Complete snapshots** vs **memory cost**: copying the whole state is simple and correct, but expensive when state is large or snapshots are frequent.
- **Shallow copies** vs **shared mutable parts**: a snapshot that shares a mutable list with the live object is not a snapshot.

## Therefore

**Have the object (the originator) produce and consume its own snapshots, and give everyone else an opaque handle to hold.**

The originator's `save()` copies its internal state into a memento, and `restore(m)` copies it back. A separate caretaker decides *when* to save and restore and keeps the mementos, but treats them as tokens. Each language enforces "opaque" differently: nested classes with private fields in Java, unexported fields in Go, a private lookup table or brand type in TypeScript, and naming convention in Python.

## Structure

```mermaid
classDiagram
  class RoutePlan {
    -Stop[] stops
    -int shiftMin
    +add(stop) void
    +minutes() int
    +save() RouteSnapshot
    +restore(s) void
  }
  class RouteSnapshot {
    -Stop[] stops
  }
  class Dispatcher {
    +planDay(route, batches) void
  }
  class Stop {
    -String id
    -int driveMin
    -int serviceMin
  }
  RoutePlan *-- Stop
  RoutePlan ..> RouteSnapshot : creates
  Dispatcher ..> RouteSnapshot : holds
  Dispatcher --> RoutePlan
```

### Participants

- **Originator** (`RoutePlan`): the object whose state is saved. Only it can create a memento or read one.
- **Memento** (`RouteSnapshot`): an immutable copy of the originator's state, opaque to everyone else.
- **Caretaker** (`Dispatcher`): decides when to snapshot and when to roll back, and stores mementos without looking inside them.

## Use it when

- You apply a change tentatively, check the result, and roll back on failure, and the change touches state the caller can't see.
- You need undo for an object whose internals are private, and writing an inverse for every operation is impractical.
- You need checkpoints in a long-running computation (a solver, an import, a simulation) to resume or backtrack.
- Several fields must be restored together, and resetting them one by one from outside would leak the object's structure.

## Do not use it when

- The state is already immutable. Keep a reference to the previous value; persistent data structures and reducer-style state (`state = reduce(state, action)`) give you snapshots for free.
- You can validate before mutating. Check the change first and skip the snapshot. It is cheaper and simpler.
- The state is a plain public record with no invariants to protect. Copy it (`structuredClone`, `copy.deepcopy`, a copy constructor); encapsulation was the only reason for the pattern.
- State is large and changes are small and frequent. Store deltas with [Command](https://aipatterns.dev/design-patterns/command.md) and `undo` instead of full copies.
- The snapshot must survive a restart or cross a process. That is serialization or an event log with a versioned format, not an in-memory memento.

## Application grill

**Inspect first:** the state being snapshotted and its size; existing undo or history features (editors, forms) and their storage; whether snapshots hold personal data that crosses a trust boundary or retention rule.

**Missing pieces:** in-memory: the language's value copies - nothing to install. Persisted: a table or blob store with a retention and deletion policy; snapshots of personal data must age out on the same schedule as the records they mirror.

**Ask before you build:**

- **Scale:** what is snapshot size times retention count - memory or storage bound?
- **Performance:** snapshot on every change or per action, and is coalescing needed?
- **Reliability:** under concurrent edits, what does restore mean - is the snapshot stale the moment it restores?
- **Budget:** who pays the retention storage and runs the deletion pipeline?
- **Implementation:** when the internal state shape changes, do old snapshots still restore (versioned snapshots), and opaque to whom?

## Implementation steps

1. List every field that has to come back on restore, including derived fields and caches, or recompute them after restore.
2. Create the memento type with those fields, immutable, and make it unreadable outside the originator.
3. Add `save()` to the originator. Copy mutable collections deeply enough that later changes don't leak into the snapshot.
4. Add `restore(memento)`. Copy again on the way in, so restoring twice from the same memento works.
5. Move the "when to save and roll back" logic into a caretaker that holds mementos as opaque values.
6. Test that `save`, then a change, then `restore` gives back an object equal to the original, and that the same memento can be restored twice.

## Example

A dispatch tool builds a delivery van's route for the day. Stops arrive in batches that must be kept together (several drops for one customer). The dispatcher adds each batch, and if the route no longer fits the shift, rolls the route back to its state before that batch.

```ts title="route.ts"
interface Stop {
  readonly id: string;
  readonly driveMin: number;
  readonly serviceMin: number;
}

// Opaque to callers: they can hold a snapshot but cannot read or build one.
declare const brand: unique symbol;
type RouteSnapshot = { readonly [brand]: true };

class RoutePlan {
  private static readonly saved = new WeakMap<RouteSnapshot, readonly Stop[]>();
  private stops: Stop[] = [];

  constructor(readonly shiftMin: number) {}

  add(stop: Stop): void {
    this.stops.push(stop);
  }
  minutes(): number {
    return this.stops.reduce((t, s) => t + s.driveMin + s.serviceMin, 0);
  }
  ids(): string[] {
    return this.stops.map((s) => s.id);
  }

  save(): RouteSnapshot {
    const snapshot = Object.freeze({}) as RouteSnapshot;
    RoutePlan.saved.set(snapshot, [...this.stops]); // stops are immutable, so a shallow copy is enough
    return snapshot;
  }

  restore(snapshot: RouteSnapshot): void {
    const stops = RoutePlan.saved.get(snapshot);
    if (!stops) throw new Error("snapshot was not created by RoutePlan");
    this.stops = [...stops];
  }
}

// Caretaker: decides when to roll back, never looks inside the snapshot.
function planDay(route: RoutePlan, batches: Stop[][]): void {
  for (const batch of batches) {
    const checkpoint = route.save();
    batch.forEach((s) => route.add(s));
    if (route.minutes() > route.shiftMin) {
      route.restore(checkpoint);
      console.log("rejected:", batch.map((s) => s.id).join(","));
    }
  }
}

const route = new RoutePlan(480);
planDay(route, [
  [{ id: "s1", driveMin: 60, serviceMin: 15 }, { id: "s2", driveMin: 40, serviceMin: 20 }],
  [{ id: "s3", driveMin: 90, serviceMin: 30 }, { id: "s4", driveMin: 50, serviceMin: 10 }],
  [{ id: "s5", driveMin: 120, serviceMin: 30 }, { id: "s6", driveMin: 45, serviceMin: 15 }],
  [{ id: "s7", driveMin: 30, serviceMin: 20 }],
]);
console.log(route.ids(), route.minutes()); // [ 's1', 's2', 's3', 's4', 's7' ] 365
```

```python title="route.py"
from dataclasses import dataclass

@dataclass(frozen=True)
class Stop:
    id: str
    drive_min: int
    service_min: int

@dataclass(frozen=True)
class RouteSnapshot:
    """Opaque by convention: only RoutePlan reads _stops."""

    _stops: tuple[Stop, ...]

class RoutePlan:
    def __init__(self, shift_min: int) -> None:
        self.shift_min = shift_min
        self._stops: list[Stop] = []

    def add(self, stop: Stop) -> None:
        self._stops.append(stop)

    def minutes(self) -> int:
        return sum(s.drive_min + s.service_min for s in self._stops)

    def ids(self) -> list[str]:
        return [s.id for s in self._stops]

    def save(self) -> RouteSnapshot:
        return RouteSnapshot(tuple(self._stops))  # tuple: the snapshot can't be mutated

    def restore(self, snapshot: RouteSnapshot) -> None:
        self._stops = list(snapshot._stops)

def plan_day(route: RoutePlan, batches: list[list[Stop]]) -> None:
    """Caretaker: decides when to roll back, never looks inside the snapshot."""
    for batch in batches:
        checkpoint = route.save()
        for stop in batch:
            route.add(stop)
        if route.minutes() > route.shift_min:
            route.restore(checkpoint)
            print("rejected:", ",".join(s.id for s in batch))

if __name__ == "__main__":
    route = RoutePlan(shift_min=480)
    plan_day(route, [
        [Stop("s1", 60, 15), Stop("s2", 40, 20)],
        [Stop("s3", 90, 30), Stop("s4", 50, 10)],
        [Stop("s5", 120, 30), Stop("s6", 45, 15)],
        [Stop("s7", 30, 20)],
    ])
    print(route.ids(), route.minutes())  # ['s1', 's2', 's3', 's4', 's7'] 365
```

```go title="route.go"
package main

import (
	"fmt"
	"slices"
	"strings"
)

type Stop struct {
	ID                   string
	DriveMin, ServiceMin int
}

// RouteSnapshot has only unexported fields, so code outside this package
// can hold one but cannot read or build it.
type RouteSnapshot struct{ stops []Stop }

type RoutePlan struct {
	ShiftMin int
	stops    []Stop
}

func (r *RoutePlan) Add(s Stop) { r.stops = append(r.stops, s) }

func (r *RoutePlan) Minutes() int {
	total := 0
	for _, s := range r.stops {
		total += s.DriveMin + s.ServiceMin
	}
	return total
}

func (r *RoutePlan) IDs() []string {
	ids := make([]string, len(r.stops))
	for i, s := range r.stops {
		ids[i] = s.ID
	}
	return ids
}

// Clone on both sides: append could otherwise write into the snapshot's backing array.
func (r *RoutePlan) Save() RouteSnapshot        { return RouteSnapshot{stops: slices.Clone(r.stops)} }
func (r *RoutePlan) Restore(snap RouteSnapshot) { r.stops = slices.Clone(snap.stops) }

// planDay is the caretaker: it decides when to roll back and never reads the snapshot.
func planDay(route *RoutePlan, batches [][]Stop) {
	for _, batch := range batches {
		checkpoint := route.Save()
		for _, s := range batch {
			route.Add(s)
		}
		if route.Minutes() > route.ShiftMin {
			route.Restore(checkpoint)
			ids := make([]string, len(batch))
			for i, s := range batch {
				ids[i] = s.ID
			}
			fmt.Println("rejected:", strings.Join(ids, ","))
		}
	}
}

func main() {
	route := &RoutePlan{ShiftMin: 480}
	planDay(route, [][]Stop{
		{{"s1", 60, 15}, {"s2", 40, 20}},
		{{"s3", 90, 30}, {"s4", 50, 10}},
		{{"s5", 120, 30}, {"s6", 45, 15}},
		{{"s7", 30, 20}},
	})
	fmt.Println(route.IDs(), route.Minutes()) // [s1 s2 s3 s4 s7] 365
}
```

```java title="Dispatch.java"
import java.util.*;
import java.util.stream.Collectors;

public class Dispatch {
    record Stop(String id, int driveMin, int serviceMin) {}

    static final class RoutePlan {
        // Private constructor and field: only RoutePlan can build or read a Snapshot.
        static final class Snapshot {
            private final List<Stop> stops;
            private Snapshot(List<Stop> stops) { this.stops = List.copyOf(stops); }
        }

        final int shiftMin;
        private List<Stop> stops = new ArrayList<>();

        RoutePlan(int shiftMin) { this.shiftMin = shiftMin; }

        void add(Stop stop) { stops.add(stop); }

        int minutes() { return stops.stream().mapToInt(s -> s.driveMin() + s.serviceMin()).sum(); }

        List<String> ids() { return stops.stream().map(Stop::id).toList(); }

        Snapshot save() { return new Snapshot(stops); }

        void restore(Snapshot snapshot) { stops = new ArrayList<>(snapshot.stops); }
    }

    // Caretaker: decides when to roll back, never looks inside the snapshot.
    static void planDay(RoutePlan route, List<List<Stop>> batches) {
        for (List<Stop> batch : batches) {
            RoutePlan.Snapshot checkpoint = route.save();
            batch.forEach(route::add);
            if (route.minutes() > route.shiftMin) {
                route.restore(checkpoint);
                System.out.println("rejected: " + batch.stream().map(Stop::id).collect(Collectors.joining(",")));
            }
        }
    }

    public static void main(String[] args) {
        RoutePlan route = new RoutePlan(480);
        planDay(route, List.of(
            List.of(new Stop("s1", 60, 15), new Stop("s2", 40, 20)),
            List.of(new Stop("s3", 90, 30), new Stop("s4", 50, 10)),
            List.of(new Stop("s5", 120, 30), new Stop("s6", 45, 15)),
            List.of(new Stop("s7", 30, 20))));
        System.out.println(route.ids() + " " + route.minutes()); // [s1, s2, s3, s4, s7] 365
    }
}
```

In Java, nested classes share private access within their top-level class, so `Dispatch` could technically read `Snapshot.stops`. In real code, put `RoutePlan` in its own file.

## Trade-offs

**Gains**
- Rollback and undo without exposing the originator's fields.
- The caretaker's logic stays simple: save, try, restore.
- One restore resets every related field together, so invariants hold.
- Snapshots are immutable and safe to keep in a history.

**Costs**
- Each snapshot copies state. Large state or frequent snapshots cost memory and time.
- Deep versus shallow copying is easy to get wrong, and the bug only shows on a later mutation.
- Opaqueness is weak in some languages (Python convention, Go package scope).
- Caretakers that keep every snapshot need a retention limit.

## Relations

- [Command](https://aipatterns.dev/design-patterns/command.md) records the operation and its inverse. Memento records the state. Use a memento inside a command when the operation has no cheap inverse.
- [Iterator](https://aipatterns.dev/design-patterns/iterator.md) positions can be saved as mementos so a long traversal can resume.
- [Prototype](https://aipatterns.dev/design-patterns/prototype.md) clones the whole object. A memento copies only the state needed to restore it, and keeps it opaque.
- [State](https://aipatterns.dev/design-patterns/state.md) objects can be stored in a memento when the current state is part of what must roll back.

## Agent checklist

- [ ] You checked that validating before mutating, or keeping an immutable previous value, doesn't already solve the problem.
- [ ] Every field that must come back is in the snapshot, or is recomputed after restore.
- [ ] Mutable collections are copied on `save` and on `restore`, and a test mutates after saving to prove the snapshot is unaffected.
- [ ] Caretakers hold mementos as opaque values, and the history has a size or age limit.
- [ ] A round-trip test (save, change, restore) asserts the originator equals its original state.
