Memento Pattern #

A user is filling out a long multi-step registration form — personal data, company data, product preferences, document uploads. On the fourth step, they realize something needs changing back on the first step. Without a snapshot mechanism, the only option is to go back to the start and fill everything in again. With the Memento Pattern, every move to the next step stores a snapshot of the current state; going back to any step means restoring that snapshot, not starting over. The code that saves and restores snapshots does not need to know what the state contains or how the form represents it — encapsulation is preserved because a Memento object can only be used by the object that created it.

What Is the Memento Pattern? #

The Memento Pattern is a behavioral design pattern that lets you save and restore an object’s previous state without exposing its internal implementation details. The object whose state you want to save creates a “snapshot” in the form of a Memento object; this snapshot can be stored by a Caretaker and handed back to the originator at any time to restore the previous state.

Three properties define the Memento Pattern:

  • Encapsulation preserved — the Caretaker that stores a Memento cannot read or modify the state contents; only the Originator can create it and restore from it
  • Complete snapshot — the Memento stores everything needed to return the object to exactly the condition it was in when the snapshot was taken
  • Separation of responsibilities — the Originator knows what needs to be stored; the Caretaker knows when and how many snapshots to keep
sequenceDiagram
    participant C as Caretaker
    participant O as Originator (Form)
    participant M as Memento (Snapshot)

    C->>O: Request snapshot
    O->>M: CreateMemento() — create a state snapshot
    O-->>C: return Memento
    C->>C: store Memento in stack

    Note over O: User changes state

    C->>O: Restore(memento)
    O->>M: Read state from Memento
    M-->>O: previous state
    Note over O: State restored to the snapshot

Difference from the Command Pattern for Undo #

Memento and the Command Pattern both support undo, but in different ways. Choosing the right one depends on the characteristics of the state being undone.

// Command Pattern for Undo:
// Store the ACTION performed + how to reverse it
type InsertCommand struct {
    editor   *Editor
    char     rune
    position int
}
func (c *InsertCommand) Undo() error {
    // Reverse the action: delete the character just inserted
    _, err := c.editor.Delete(c.position)
    return err
}
// Good for: discrete actions that are easy to reverse logically

// Memento Pattern for Undo:
// Store a STATE SNAPSHOT before the action
type EditorMemento struct {
    content string
    cursor  int
}
// Undo = restore the snapshot
// Good for: complex state that is hard to reverse action by action
AspectCommand PatternMemento Pattern
What is storedThe action + how to reverse itA state snapshot
Best forDiscrete, easily inverted actionsComplex state that is hard to invert
MemoryMore economical — only deltasMore expensive — the entire state
Partial undoCan undo part of an actionMust restore the whole snapshot
ExamplesText insert/deleteForm wizard, game saves

Three Components of the Memento Pattern #

classDiagram
    class FormWizard {
        -currentStep int
        -personalData PersonalData
        -companyData CompanyData
        -preferences Preferences
        +CreateMemento() WizardMemento
        +RestoreFromMemento(m WizardMemento)
        +NextStep()
        +PrevStep()
    }

    class WizardMemento {
        -step int
        -personalData PersonalData
        -companyData CompanyData
        -preferences Preferences
        -createdAt time.Time
        +CreatedAt() time.Time
        +StepNumber() int
    }

    class WizardHistory {
        -snapshots []WizardMemento
        -maxSize int
        +Save(originator FormWizard)
        +Undo(originator FormWizard)
        +Count() int
    }

    FormWizard ..> WizardMemento : creates
    FormWizard <.. WizardMemento : restores state
    WizardHistory o-- WizardMemento : manages
    WizardHistory --> FormWizard : interacts with
ComponentRoleWho May Access
OriginatorCreates and restores MementosAll Memento fields (full access)
MementoStores a snapshot of the Originator’s stateOriginator only — fields are unexported
CaretakerStores and returns MementosOnly the Memento interface — cannot read the state

Full Implementation: Multi-Step Form Wizard #

Originator: FormWizard #

package wizard

import (
    "fmt"
    "time"
)

// PersonalData stores the user's personal data.
type PersonalData struct {
    FullName    string
    Email       string
    PhoneNumber string
    BirthDate   string
}

// CompanyData stores the company data.
type CompanyData struct {
    CompanyName string
    Industry    string
    Size        string
    Website     string
}

// Preferences stores the user's product preferences.
type Preferences struct {
    ProductTier   string
    BillingCycle  string
    Addons        []string
    NewsletterOpt bool
}

// WizardMemento stores a snapshot of the wizard state at one point in time.
// These fields are unexported — only FormWizard can read and write them.
type WizardMemento struct {
    step         int
    personal     PersonalData
    company      CompanyData
    preferences  Preferences
    createdAt    time.Time
}

// CreatedAt returns when the snapshot was created — the only method the Caretaker may access.
func (m *WizardMemento) CreatedAt() time.Time { return m.createdAt }

// StepNumber returns the step at which the snapshot was created.
func (m *WizardMemento) StepNumber() int { return m.step }

// FormWizard is the Originator — the object whose state we want to save.
type FormWizard struct {
    currentStep int
    personal    PersonalData
    company     CompanyData
    preferences Preferences
    maxSteps    int
}

func NewFormWizard() *FormWizard {
    return &FormWizard{
        currentStep: 1,
        maxSteps:    4,
    }
}

// CreateMemento creates a snapshot of the wizard's current state.
// A deep copy is made to ensure the Memento is independent of the wizard's state.
func (w *FormWizard) CreateMemento() *WizardMemento {
    // Deep copy Preferences.Addons (slice — needs copying)
    addonsCopy := make([]string, len(w.preferences.Addons))
    copy(addonsCopy, w.preferences.Addons)

    return &WizardMemento{
        step: w.currentStep,
        personal: PersonalData{
            FullName:    w.personal.FullName,
            Email:       w.personal.Email,
            PhoneNumber: w.personal.PhoneNumber,
            BirthDate:   w.personal.BirthDate,
        },
        company: CompanyData{
            CompanyName: w.company.CompanyName,
            Industry:    w.company.Industry,
            Size:        w.company.Size,
            Website:     w.company.Website,
        },
        preferences: Preferences{
            ProductTier:   w.preferences.ProductTier,
            BillingCycle:  w.preferences.BillingCycle,
            Addons:        addonsCopy,
            NewsletterOpt: w.preferences.NewsletterOpt,
        },
        createdAt: time.Now(),
    }
}

// RestoreFromMemento restores the wizard state from a Memento.
func (w *FormWizard) RestoreFromMemento(m *WizardMemento) {
    // Deep copy again to avoid sharing the slice
    addonsCopy := make([]string, len(m.preferences.Addons))
    copy(addonsCopy, m.preferences.Addons)

    w.currentStep    = m.step
    w.personal       = m.personal
    w.company        = m.company
    w.preferences    = m.preferences
    w.preferences.Addons = addonsCopy
}

// Step 1: Personal Data
func (w *FormWizard) SetPersonalData(data PersonalData) error {
    if data.FullName == "" || data.Email == "" {
        return fmt.Errorf("full name and email are required")
    }
    w.personal = data
    return nil
}

// Step 2: Company Data
func (w *FormWizard) SetCompanyData(data CompanyData) error {
    if data.CompanyName == "" {
        return fmt.Errorf("company name is required")
    }
    w.company = data
    return nil
}

// Step 3: Preferences
func (w *FormWizard) SetPreferences(prefs Preferences) error {
    if prefs.ProductTier == "" {
        return fmt.Errorf("product tier must be selected")
    }
    w.preferences = prefs
    return nil
}

// NextStep moves to the next step.
func (w *FormWizard) NextStep() error {
    if w.currentStep >= w.maxSteps {
        return fmt.Errorf("already at final step")
    }
    w.currentStep++
    return nil
}

// PrevStep moves back to the previous step.
func (w *FormWizard) PrevStep() error {
    if w.currentStep <= 1 {
        return fmt.Errorf("already at first step")
    }
    w.currentStep--
    return nil
}

// CurrentStep returns the current step.
func (w *FormWizard) CurrentStep() int { return w.currentStep }

// Summary returns a summary of the wizard's current data.
func (w *FormWizard) Summary() string {
    return fmt.Sprintf("Step %d/%d | %s (%s) | %s | %s",
        w.currentStep, w.maxSteps,
        w.personal.FullName, w.personal.Email,
        w.company.CompanyName,
        w.preferences.ProductTier,
    )
}

Caretaker: WizardHistory #

package wizard

import (
    "fmt"
    "time"
)

// SnapshotInfo stores snapshot metadata for display to the user.
type SnapshotInfo struct {
    Index     int
    Step      int
    CreatedAt time.Time
}

// WizardHistory is the Caretaker — it stores and manages Mementos.
// It cannot read the state inside a Memento — only store and return it.
type WizardHistory struct {
    snapshots []*WizardMemento
    maxSize   int
}

func NewWizardHistory(maxSize int) *WizardHistory {
    return &WizardHistory{
        snapshots: make([]*WizardMemento, 0, maxSize),
        maxSize:   maxSize,
    }
}

// Save stores a snapshot of the wizard's current state.
func (h *WizardHistory) Save(wizard *FormWizard) {
    if len(h.snapshots) >= h.maxSize {
        // Remove the oldest snapshot to make room
        h.snapshots = h.snapshots[1:]
    }
    h.snapshots = append(h.snapshots, wizard.CreateMemento())
    fmt.Printf("[History] Snapshot saved for step %d (total: %d)\n",
        wizard.CurrentStep(), len(h.snapshots))
}

// Undo restores the latest snapshot to the wizard.
func (h *WizardHistory) Undo(wizard *FormWizard) error {
    if len(h.snapshots) == 0 {
        return fmt.Errorf("no snapshot available to restore")
    }
    // Pop the latest snapshot
    last := h.snapshots[len(h.snapshots)-1]
    h.snapshots = h.snapshots[:len(h.snapshots)-1]

    wizard.RestoreFromMemento(last)
    fmt.Printf("[History] State restored to step %d (remaining snapshots: %d)\n",
        last.StepNumber(), len(h.snapshots))
    return nil
}

// GoToStep restores the snapshot for a specific step.
func (h *WizardHistory) GoToStep(wizard *FormWizard, targetStep int) error {
    for i := len(h.snapshots) - 1; i >= 0; i-- {
        if h.snapshots[i].StepNumber() == targetStep {
            wizard.RestoreFromMemento(h.snapshots[i])
            // Remove all snapshots after this one
            h.snapshots = h.snapshots[:i+1]
            return nil
        }
    }
    return fmt.Errorf("no snapshot for step %d", targetStep)
}

// ListSnapshots returns the list of stored snapshots for display to the user.
// The Caretaker cannot access the state inside Mementos — only metadata.
func (h *WizardHistory) ListSnapshots() []SnapshotInfo {
    info := make([]SnapshotInfo, len(h.snapshots))
    for i, snap := range h.snapshots {
        info[i] = SnapshotInfo{
            Index:     i,
            Step:      snap.StepNumber(),
            CreatedAt: snap.CreatedAt(),
        }
    }
    return info
}

// Count returns the number of stored snapshots.
func (h *WizardHistory) Count() int { return len(h.snapshots) }

Demonstration #

func main() {
    wizard := wizard.NewFormWizard()
    history := wizard.NewWizardHistory(10)

    // Step 1: Fill in personal data and save a snapshot
    _ = wizard.SetPersonalData(wizard.PersonalData{
        FullName:    "Budi Santoso",
        Email:       "[email protected]",
        PhoneNumber: "+6281234567890",
    })
    history.Save(wizard)
    _ = wizard.NextStep()
    fmt.Printf("After step 1: %s\n", wizard.Summary())

    // Step 2: Fill in company data and save a snapshot
    _ = wizard.SetCompanyData(wizard.CompanyData{
        CompanyName: "PT Maju Bersama",
        Industry:    "Technology",
        Size:        "51-200",
    })
    history.Save(wizard)
    _ = wizard.NextStep()
    fmt.Printf("After step 2: %s\n", wizard.Summary())

    // Step 3: Fill in preferences
    _ = wizard.SetPreferences(wizard.Preferences{
        ProductTier:  "Enterprise",
        BillingCycle: "annual",
        Addons:       []string{"sso", "audit-log"},
    })
    history.Save(wizard)
    _ = wizard.NextStep()
    fmt.Printf("After step 3: %s\n", wizard.Summary())

    // The user wants to go back to step 1 to correct the email
    fmt.Println("\n--- User wants to go back to step 1 ---")
    _ = history.GoToStep(wizard, 1)
    fmt.Printf("After going back to step 1: %s\n", wizard.Summary())

    // Correct the email
    _ = wizard.SetPersonalData(wizard.PersonalData{
        FullName:    "Budi Santoso",
        Email:       "[email protected]", // new email
        PhoneNumber: "+6281234567890",
    })
    fmt.Printf("After email correction: %s\n", wizard.Summary())

    // Undo one step
    fmt.Println("\n--- Undo ---")
    _ = history.Undo(wizard)
    fmt.Printf("After undo: %s\n", wizard.Summary())
}

Second Case Study: Game Checkpoint System #

Games are the most classic Memento use case — every checkpoint stores the entire game state (player position, health, inventory, level progress) so the player can return to a checkpoint when they die.

package game

import (
    "fmt"
    "time"
)

// PlayerState stores the player's complete state for snapshots.
type PlayerState struct {
    Level     int
    Health    int
    MaxHealth int
    Position  Position
    Inventory []Item
    Gold      int
    Skills    map[string]int
    QuestLog  []Quest
}

// Position represents the player's position in the game world.
type Position struct {
    X, Y, Z float64
    Map      string
}

// Item represents one item in the inventory.
type Item struct {
    ID       string
    Name     string
    Quantity int
    Equipped bool
}

// Quest represents a quest in progress or completed.
type Quest struct {
    ID       string
    Name     string
    Progress int
    Complete bool
}

// GameMemento stores a snapshot of the game state.
type GameMemento struct {
    state       PlayerState
    savedAt     time.Time
    checkpointName string
}

func (m *GameMemento) SavedAt() time.Time       { return m.savedAt }
func (m *GameMemento) CheckpointName() string   { return m.checkpointName }

// Player is the Originator in the game context.
type Player struct {
    state PlayerState
}

func NewPlayer() *Player {
    return &Player{
        state: PlayerState{
            Level:     1,
            Health:    100,
            MaxHealth: 100,
            Position:  Position{X: 0, Y: 0, Z: 0, Map: "starting_village"},
            Inventory: make([]Item, 0),
            Gold:      50,
            Skills:    map[string]int{"sword": 1, "shield": 1},
            QuestLog:  make([]Quest, 0),
        },
    }
}

// Save creates a snapshot of the player's current state.
func (p *Player) Save(checkpointName string) *GameMemento {
    // Deep copy all reference types
    inventoryCopy := make([]Item, len(p.state.Inventory))
    copy(inventoryCopy, p.state.Inventory)

    skillsCopy := make(map[string]int, len(p.state.Skills))
    for k, v := range p.state.Skills {
        skillsCopy[k] = v
    }

    questsCopy := make([]Quest, len(p.state.QuestLog))
    copy(questsCopy, p.state.QuestLog)

    return &GameMemento{
        state: PlayerState{
            Level:     p.state.Level,
            Health:    p.state.Health,
            MaxHealth: p.state.MaxHealth,
            Position:  p.state.Position, // struct — value copy is safe
            Inventory: inventoryCopy,
            Gold:      p.state.Gold,
            Skills:    skillsCopy,
            QuestLog:  questsCopy,
        },
        savedAt:        time.Now(),
        checkpointName: checkpointName,
    }
}

// Restore restores the player state from a Memento.
func (p *Player) Restore(m *GameMemento) {
    inventoryCopy := make([]Item, len(m.state.Inventory))
    copy(inventoryCopy, m.state.Inventory)

    skillsCopy := make(map[string]int, len(m.state.Skills))
    for k, v := range m.state.Skills {
        skillsCopy[k] = v
    }

    questsCopy := make([]Quest, len(m.state.QuestLog))
    copy(questsCopy, m.state.QuestLog)

    p.state = PlayerState{
        Level:     m.state.Level,
        Health:    m.state.Health,
        MaxHealth: m.state.MaxHealth,
        Position:  m.state.Position,
        Inventory: inventoryCopy,
        Gold:      m.state.Gold,
        Skills:    skillsCopy,
        QuestLog:  questsCopy,
    }
    fmt.Printf("[Game] Restored to checkpoint '%s' at %s\n",
        m.checkpointName, m.savedAt.Format("15:04:05"))
}

// Player actions
func (p *Player) TakeDamage(amount int) {
    p.state.Health -= amount
    if p.state.Health < 0 {
        p.state.Health = 0
    }
}

func (p *Player) AddItem(item Item) {
    p.state.Inventory = append(p.state.Inventory, item)
}

func (p *Player) SpendGold(amount int) error {
    if p.state.Gold < amount {
        return fmt.Errorf("not enough gold")
    }
    p.state.Gold -= amount
    return nil
}

func (p *Player) MoveTo(pos Position) {
    p.state.Position = pos
}

func (p *Player) LevelUp() {
    p.state.Level++
    p.state.MaxHealth += 20
    p.state.Health = p.state.MaxHealth
}

func (p *Player) StatusReport() string {
    return fmt.Sprintf("Level %d | HP %d/%d | Gold %d | Map: %s (%.0f,%.0f)",
        p.state.Level, p.state.Health, p.state.MaxHealth,
        p.state.Gold, p.state.Position.Map,
        p.state.Position.X, p.state.Position.Y,
    )
}

func (p *Player) IsDead() bool { return p.state.Health <= 0 }


// SaveGameManager is the Caretaker for the game — it manages save slots.
type SaveGameManager struct {
    slots   map[string]*GameMemento // slot name → memento
    maxSlots int
}

func NewSaveGameManager(maxSlots int) *SaveGameManager {
    return &SaveGameManager{
        slots:    make(map[string]*GameMemento),
        maxSlots: maxSlots,
    }
}

// SaveToSlot stores a snapshot into a specific slot.
func (m *SaveGameManager) SaveToSlot(player *Player, slotName string) {
    memento := player.Save(slotName)
    m.slots[slotName] = memento
    fmt.Printf("[SaveManager] Game saved to slot '%s'\n", slotName)
}

// LoadFromSlot restores the state from a specific slot.
func (m *SaveGameManager) LoadFromSlot(player *Player, slotName string) error {
    memento, ok := m.slots[slotName]
    if !ok {
        return fmt.Errorf("save slot '%s' not found", slotName)
    }
    player.Restore(memento)
    return nil
}

// ListSlots returns the list of available save slots.
func (m *SaveGameManager) ListSlots() []string {
    slots := make([]string, 0, len(m.slots))
    for name := range m.slots {
        slots = append(slots, name)
    }
    return slots
}


// Demonstration
func playGame() {
    player := NewPlayer()
    saveManager := NewSaveGameManager(5)

    fmt.Printf("Start: %s\n", player.StatusReport())

    // Checkpoint 1: In the starting village
    saveManager.SaveToSlot(player, "checkpoint_village")

    // Player progresses
    player.LevelUp()
    player.AddItem(Item{ID: "iron_sword", Name: "Iron Sword", Quantity: 1, Equipped: true})
    player.MoveTo(Position{X: 150, Y: 0, Z: 0, Map: "dark_forest"})
    fmt.Printf("After exploring: %s\n", player.StatusReport())

    // Checkpoint 2: Before the boss fight
    saveManager.SaveToSlot(player, "checkpoint_before_boss")

    // Player loses the boss fight
    player.TakeDamage(200)
    fmt.Printf("After boss fight: %s\n", player.StatusReport())

    if player.IsDead() {
        fmt.Println("\n--- Player died! Loading checkpoint... ---")
        _ = saveManager.LoadFromSlot(player, "checkpoint_before_boss")
        fmt.Printf("After loading: %s\n", player.StatusReport())
    }
}

Serializing Mementos to Persistent Storage #

For game saves that survive the application closing, Mementos need to be serialized to disk or a database.

package game

import (
    "encoding/json"
    "os"
    "path/filepath"
    "fmt"
)

// PersistentSaveManager stores Mementos on the file system.
type PersistentSaveManager struct {
    saveDir string
}

func NewPersistentSaveManager(saveDir string) (*PersistentSaveManager, error) {
    if err := os.MkdirAll(saveDir, 0755); err != nil {
        return nil, fmt.Errorf("cannot create save directory: %w", err)
    }
    return &PersistentSaveManager{saveDir: saveDir}, nil
}

// serializedMemento is a JSON-serializable version of GameMemento.
// This is needed because GameMemento's fields are unexported.
type serializedMemento struct {
    State          PlayerState `json:"state"`
    SavedAt        string      `json:"saved_at"`
    CheckpointName string      `json:"checkpoint_name"`
}

// SaveToDisk stores a Memento to a file.
func (m *PersistentSaveManager) SaveToDisk(player *Player, slotName string) error {
    memento := player.Save(slotName)

    // Serialization — we need access to the internal fields
    // In a real implementation, the Originator provides an ExportForSave() method
    serialized := serializedMemento{
        State:          memento.state,
        SavedAt:        memento.savedAt.Format(time.RFC3339),
        CheckpointName: memento.checkpointName,
    }

    data, err := json.MarshalIndent(serialized, "", "  ")
    if err != nil {
        return fmt.Errorf("cannot serialize save: %w", err)
    }

    filename := filepath.Join(m.saveDir, slotName+".json")
    if err := os.WriteFile(filename, data, 0644); err != nil {
        return fmt.Errorf("cannot write save file: %w", err)
    }

    fmt.Printf("[PersistentSave] Saved to %s\n", filename)
    return nil
}

// LoadFromDisk loads a Memento from a file and restores it to the player.
func (m *PersistentSaveManager) LoadFromDisk(player *Player, slotName string) error {
    filename := filepath.Join(m.saveDir, slotName+".json")
    data, err := os.ReadFile(filename)
    if err != nil {
        return fmt.Errorf("save file not found: %w", err)
    }

    var serialized serializedMemento
    if err := json.Unmarshal(data, &serialized); err != nil {
        return fmt.Errorf("cannot deserialize save: %w", err)
    }

    // Reconstruct the Memento from the deserialized data
    savedAt, _ := time.Parse(time.RFC3339, serialized.SavedAt)
    memento := &GameMemento{
        state:          serialized.State,
        savedAt:        savedAt,
        checkpointName: serialized.CheckpointName,
    }

    player.Restore(memento)
    fmt.Printf("[PersistentSave] Loaded from %s\n", filename)
    return nil
}

Memory Management: Limiting Snapshot Count #

Storing too many snapshots can exhaust memory, especially for large states. There are several strategies to deal with this.

// Strategy 1: Fixed-size ring buffer — drop the oldest when full
type BoundedHistory struct {
    snapshots []*WizardMemento
    maxSize   int
    writeIdx  int
    count     int
}

func NewBoundedHistory(maxSize int) *BoundedHistory {
    return &BoundedHistory{
        snapshots: make([]*WizardMemento, maxSize),
        maxSize:   maxSize,
    }
}

func (h *BoundedHistory) Save(wizard *FormWizard) {
    h.snapshots[h.writeIdx] = wizard.CreateMemento()
    h.writeIdx = (h.writeIdx + 1) % h.maxSize
    if h.count < h.maxSize {
        h.count++
    }
}


// Strategy 2: Snapshots with delta compression
// Store a full state only every N steps; store only deltas in between
type DeltaHistory struct {
    baseSnapshot   *WizardMemento
    deltas         []WizardDelta
    fullSnapshotN  int // store a full snapshot every N changes
}

type WizardDelta struct {
    changedFields map[string]interface{}
    timestamp     time.Time
}


// Strategy 3: Expired snapshots — remove snapshots that are too old
type TimedHistory struct {
    snapshots []*timedMemento
    maxAge    time.Duration
}

type timedMemento struct {
    memento   *WizardMemento
    expiresAt time.Time
}

func (h *TimedHistory) PurgeExpired() {
    now := time.Now()
    var active []*timedMemento
    for _, tm := range h.snapshots {
        if now.Before(tm.expiresAt) {
            active = append(active, tm)
        }
    }
    h.snapshots = active
}

Large State = Expensive Snapshots

If the snapshotted object has large slices, maps with many entries, or deeply nested structs, every snapshot can consume significant memory. For such cases, consider: (1) snapshotting only the fields that changed (delta snapshots), (2) limiting the maximum snapshot count with a ring buffer, (3) compressing snapshots before storing, or (4) serializing directly to disk for very large states.


Testing the Memento Pattern #

func TestFormWizard_CreateAndRestoreMemento(t *testing.T) {
    wizard := NewFormWizard()
    history := NewWizardHistory(5)

    // Set the initial state
    _ = wizard.SetPersonalData(PersonalData{FullName: "Alice", Email: "[email protected]"})
    history.Save(wizard)
    _ = wizard.NextStep()

    // Change the state
    _ = wizard.SetCompanyData(CompanyData{CompanyName: "Test Corp"})
    _ = wizard.NextStep()

    // Verify the state changed
    if wizard.CurrentStep() != 3 {
        t.Errorf("expected step 3, got %d", wizard.CurrentStep())
    }

    // Undo to step 1
    _ = history.Undo(wizard)
    if wizard.CurrentStep() != 1 {
        t.Errorf("after undo: expected step 1, got %d", wizard.CurrentStep())
    }
    if wizard.personal.FullName != "Alice" {
        t.Errorf("personal data should be restored, got %q", wizard.personal.FullName)
    }
}

func TestWizardHistory_BoundedByMaxSize(t *testing.T) {
    wizard := NewFormWizard()
    history := NewWizardHistory(3) // maximum 3 snapshots

    for i := 0; i < 5; i++ {
        _ = wizard.SetPersonalData(PersonalData{
            FullName: fmt.Sprintf("User %d", i),
            Email:    fmt.Sprintf("user%[email protected]", i),
        })
        history.Save(wizard)
    }

    if history.Count() > 3 {
        t.Errorf("expected max 3 snapshots, got %d", history.Count())
    }
}

func TestWizardHistory_GoToStep(t *testing.T) {
    wizard := NewFormWizard()
    history := NewWizardHistory(10)

    for step := 1; step <= 3; step++ {
        history.Save(wizard)
        _ = wizard.NextStep()
    }

    err := history.GoToStep(wizard, 2)
    if err != nil {
        t.Fatalf("GoToStep failed: %v", err)
    }
    if wizard.CurrentStep() != 2 {
        t.Errorf("expected step 2 after GoToStep, got %d", wizard.CurrentStep())
    }
}

func TestGameMemento_DeepCopyInventory(t *testing.T) {
    player := NewPlayer()
    player.AddItem(Item{ID: "sword", Name: "Iron Sword", Quantity: 1})

    // Create a snapshot
    memento := player.Save("test")

    // Modify the inventory after the snapshot
    player.state.Inventory[0].Quantity = 99

    // Restore — the inventory must return to quantity 1, not 99
    player.Restore(memento)

    if player.state.Inventory[0].Quantity != 1 {
        t.Errorf("expected quantity 1 after restore, got %d (shallow copy bug!)",
            player.state.Inventory[0].Quantity)
    }
}

func TestGameMemento_UndoAfterDeath(t *testing.T) {
    player := NewPlayer()
    saveManager := NewSaveGameManager(3)

    initialHealth := player.state.Health
    saveManager.SaveToSlot(player, "before_boss")

    player.TakeDamage(200)
    if !player.IsDead() {
        t.Error("player should be dead after 200 damage")
    }

    err := saveManager.LoadFromSlot(player, "before_boss")
    if err != nil {
        t.Fatalf("LoadFromSlot failed: %v", err)
    }
    if player.state.Health != initialHealth {
        t.Errorf("health should be restored to %d, got %d", initialHealth, player.state.Health)
    }
    if player.IsDead() {
        t.Error("player should not be dead after restore")
    }
}

When to Use and When Not to #

USE Memento if:
  ✓ You need to store snapshots of complex state for undo/redo or rollback
  ✓ The object's state is too complex to reverse logically (the Command Pattern is not enough)
  ✓ Encapsulation must be preserved — the Caretaker must not see the state contents
  ✓ You need multiple navigable save points (form wizard, game checkpoints)
  ✓ State needs to be stored to persistent storage for later restore

AVOID Memento if:
  ✗ The object's state is very large — every snapshot consumes significant memory
  ✗ The state changes very frequently — too many snapshots need to be created
  ✗ The actions needing undo are discrete and easy to reverse — use Command
  ✗ You only need one-step undo — storing a single copy of the previous state is enough

Memento Review Checklist #

DESIGN:
  □ Memento fields are unexported — only the Originator can read/write the state
  □ The Caretaker only accesses Memento metadata (CreatedAt, Name), not the state
  □ Mementos are immutable after creation — no setters
  □ The Originator does deep copies in CreateMemento and RestoreFromMemento

DEEP COPY:
  □ All slices are copied (not shared references)
  □ All maps are copied (not shared references)
  □ All nested pointer structs are copied explicitly
  □ No shared mutable state between the Originator and the Memento

MEMORY:
  □ There is a maximum snapshot count (ring buffer or eviction policy)
  □ Snapshots no longer needed are removed from the Caretaker
  □ Large states consider delta snapshots or compression

TESTING:
  □ State after Restore is exactly the same as the state at CreateMemento
  □ Modifying state after CreateMemento does not affect the Memento (deep copy)
  □ Modifying the Memento does not affect the Originator's state (deep copy in Restore)
  □ The maximum snapshot limit is verified

Summary #

  • Memento stores state snapshots without breaking encapsulation — the Caretaker stores Mementos but cannot read their contents; only the Originator can create and restore Mementos.
  • Three components: Originator (creates and restores Mementos), Memento (stores state with unexported fields), and Caretaker (stores and returns Mementos without knowing their contents).
  • Deep copy is a duty — every slice, map, and pointer in the state must be copied deeply during CreateMemento and RestoreFromMemento; a shallow copy creates shared mutable state that becomes a source of bugs.
  • Different from Command: Command stores the action and how to reverse it; Memento stores a complete state snapshot — choose based on the complexity of the state being undone.
  • Limit the snapshot count — use a ring buffer or eviction policy to prevent memory leaks from unlimited snapshot accumulation.
  • Serialization to disk lets Mementos survive after the application closes — useful for game saves, form drafts, and session recovery.
  • Form wizards and game checkpoints are the two most natural use cases — both need the ability to return to a specific point without starting over.
  • For very large states, consider delta snapshots (store only what changed) or compression before storing.

← Previous: Mediator   Next: Iterator →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact