Memento Pattern Best Practices in C#: Code Organization and Maintainability
You know how the memento pattern works. You've got an originator that produces snapshots, a memento that holds state, and a caretaker that manages the history. But bridging the gap between a textbook implementation and one that survives production is where things get interesting. Memento pattern best practices in C# go beyond saving and restoring state -- they address how to keep mementos immutable, how to avoid memory bloat from unbounded history, how to handle complex nested objects, and how to test that state preservation actually works as advertised.
This guide covers the practical decisions that determine whether your memento pattern code stays clean or quietly becomes a maintenance headache. We'll walk through immutable memento design, bounded history management, serialization strategies for complex state, proper encapsulation of memento creation, deep versus shallow copy considerations, testing strategies, and performance tuning for large state objects. If you need a refresher on behavioral design patterns in general, check those fundamentals first and come back when you're ready to sharpen your approach.
Keep Mementos Immutable
The single most important memento pattern best practice in C# is making your memento objects completely immutable. A memento represents a frozen snapshot of state at a specific point in time. If anything can modify that snapshot after creation, the entire undo/redo mechanism becomes unreliable.
Here's the anti-pattern -- a mutable memento that invites corruption:
using System;
// Bad: Mutable memento allows state tampering
public class EditorMemento
{
public string Content { get; set; }
public int CursorPosition { get; set; }
public string FontName { get; set; }
}
Nothing prevents the caretaker or any other code from modifying Content after the snapshot is taken. The corrected version seals everything:
using System;
// Good: Immutable memento preserves snapshot integrity
public sealed class EditorMemento
{
public string Content { get; }
public int CursorPosition { get; }
public string FontName { get; }
public EditorMemento(
string content,
int cursorPosition,
string fontName)
{
Content = content
?? throw new ArgumentNullException(
nameof(content));
CursorPosition = cursorPosition;
FontName = fontName
?? throw new ArgumentNullException(
nameof(fontName));
}
}
By using get-only properties and requiring all values through the constructor, you guarantee the snapshot cannot be tampered with after creation. Mark the class sealed so no subclass can introduce mutable state. This is the same immutability principle that makes the command pattern effective when commands are queued or replayed.
For simpler mementos, C# records offer a concise alternative:
using System;
// Good: Record-based immutable memento
public sealed record EditorMemento(
string Content,
int CursorPosition,
string FontName);
Records give you immutability, value equality, and a clean ToString() representation with minimal boilerplate. Use records when your memento is purely data. Use a sealed class with constructor validation when you need guard clauses or more complex initialization logic. Either way, the memento pattern best practice is the same -- once created, a memento never changes.
Limit Memento History Size
Unbounded memento history is one of the most common mistakes in memento pattern implementations. Every snapshot consumes memory, and if your originator captures state frequently -- after every keystroke in a text editor, for example -- you can exhaust available memory surprisingly fast.
Here's the anti-pattern -- a caretaker that never discards history:
using System;
using System.Collections.Generic;
// Bad: Unbounded history grows without limit
public class EditorHistory
{
private readonly List<EditorMemento> _mementos = new();
public void Save(EditorMemento memento)
{
_mementos.Add(memento);
}
public EditorMemento Undo()
{
if (_mementos.Count == 0)
{
return null;
}
EditorMemento memento = _mementos[^1];
_mementos.RemoveAt(_mementos.Count - 1);
return memento;
}
}
And the corrected version -- a bounded history that evicts the oldest snapshots:
using System;
using System.Collections.Generic;
// Good: Bounded history prevents memory bloat
public sealed class EditorHistory
{
private readonly LinkedList<EditorMemento> _mementos = new();
private readonly int _maxHistorySize;
public EditorHistory(int maxHistorySize)
{
if (maxHistorySize <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(maxHistorySize),
"History size must be positive.");
}
_maxHistorySize = maxHistorySize;
}
public int Count => _mementos.Count;
public void Save(EditorMemento memento)
{
ArgumentNullException.ThrowIfNull(memento);
if (_mementos.Count >= _maxHistorySize)
{
_mementos.RemoveFirst();
}
_mementos.AddLast(memento);
}
public EditorMemento Undo()
{
if (_mementos.Count == 0)
{
return null;
}
EditorMemento memento = _mementos.Last.Value;
_mementos.RemoveLast();
return memento;
}
}
The LinkedList<T> gives O(1) removal from both ends, making it an efficient structure for a sliding window of mementos. The _maxHistorySize parameter lets the caller configure how many snapshots to keep based on their application's memory budget.
A few guidelines for choosing a history limit as part of your memento pattern best practices:
- Text editors and drawing tools: 50-100 snapshots is common. Users rarely undo more than a few dozen actions.
- Game save states: Keep fewer but more complete snapshots -- 5-10 saves are typical.
- Form wizards: One memento per step is usually sufficient.
The key insight is that your caretaker should own the eviction policy. The originator creates mementos. The caretaker decides how many to keep. This separation of concerns keeps both components simple and independently configurable.
Use Serialization for Complex State
When your originator contains nested objects, collections, or references to other entities, a simple property-by-property copy won't capture the full state. Serialization provides a reliable way to create deep snapshots without manually cloning every object in the graph.
Here's a scenario where naive copying fails:
using System;
using System.Collections.Generic;
// Bad: Shallow reference copy -- shared mutation breaks snapshots
public class DocumentState
{
public string Title { get; set; }
public List<string> Paragraphs { get; set; }
}
public class Document
{
private DocumentState _state = new();
public DocumentState CreateMemento()
{
return _state;
}
public void RestoreMemento(DocumentState memento)
{
_state = memento;
}
}
The memento shares the same Paragraphs list as the originator. Modifying the document after saving a memento corrupts the snapshot. The corrected approach uses JSON serialization to create a fully independent copy:
using System;
using System.Collections.Generic;
using System.Text.Json;
public sealed class DocumentMemento
{
public string SerializedState { get; }
public DateTimeOffset CreatedAt { get; }
public DocumentMemento(
string serializedState)
{
SerializedState = serializedState
?? throw new ArgumentNullException(
nameof(serializedState));
CreatedAt = DateTimeOffset.UtcNow;
}
}
public sealed class Document
{
public string Title { get; set; }
public List<string> Paragraphs { get; set; } = new();
public DocumentMemento CreateMemento()
{
string json = JsonSerializer.Serialize(
new DocumentSnapshot(
Title,
new List<string>(Paragraphs)));
return new DocumentMemento(json);
}
public void RestoreMemento(DocumentMemento memento)
{
ArgumentNullException.ThrowIfNull(memento);
DocumentSnapshot snapshot =
JsonSerializer.Deserialize<DocumentSnapshot>(
memento.SerializedState);
Title = snapshot.Title;
Paragraphs = new List<string>(
snapshot.Paragraphs);
}
private sealed record DocumentSnapshot(
string Title,
List<string> Paragraphs);
}
Serialization gives you a complete deep copy regardless of how complex the object graph is. The DocumentSnapshot record acts as a serialization DTO that captures exactly what needs to be preserved. This is a memento pattern best practice in C# that scales well -- as the originator's state grows, you add properties to the snapshot record rather than writing manual cloning logic for each new field.
Be aware of the trade-offs. Serialization is slower than direct property assignment, and the serialized string consumes more memory than a compact object. For most applications, this overhead is negligible. For performance-critical scenarios with very frequent snapshots, measure before choosing this approach. We'll cover performance strategies in a later section.
Encapsulate Memento Creation in the Originator
The originator should be the only class that knows how to create and interpret mementos. When caretakers or other external code reach into the originator's internals to build mementos manually, you break encapsulation and create tight coupling that makes refactoring painful.
Here's the anti-pattern -- the caretaker building mementos by pulling state out of the originator:
using System;
// Bad: Caretaker knows originator's internal structure
public class SpreadsheetCaretaker
{
private readonly Spreadsheet _spreadsheet;
public SpreadsheetCaretaker(Spreadsheet spreadsheet)
{
_spreadsheet = spreadsheet;
}
public SpreadsheetMemento Save()
{
return new SpreadsheetMemento(
_spreadsheet.CellValues,
_spreadsheet.ColumnWidths,
_spreadsheet.RowHeights,
_spreadsheet.SelectedCell);
}
}
If the Spreadsheet class adds a new property like ConditionalFormats, the caretaker silently produces incomplete snapshots. The corrected approach delegates everything to the originator:
using System;
using System.Collections.Generic;
public sealed class SpreadsheetMemento
{
internal string SerializedState { get; }
internal SpreadsheetMemento(string serializedState)
{
SerializedState = serializedState;
}
}
public sealed class Spreadsheet
{
private Dictionary<string, string> _cellValues = new();
private Dictionary<int, double> _columnWidths = new();
private string _selectedCell = "A1";
public SpreadsheetMemento CreateMemento()
{
string state = System.Text.Json.JsonSerializer
.Serialize(new
{
CellValues = _cellValues,
ColumnWidths = _columnWidths,
SelectedCell = _selectedCell,
});
return new SpreadsheetMemento(state);
}
public void RestoreMemento(SpreadsheetMemento memento)
{
ArgumentNullException.ThrowIfNull(memento);
var snapshot = System.Text.Json.JsonSerializer
.Deserialize<SpreadsheetSnapshot>(
memento.SerializedState);
_cellValues = new Dictionary<string, string>(
snapshot.CellValues);
_columnWidths = new Dictionary<int, double>(
snapshot.ColumnWidths);
_selectedCell = snapshot.SelectedCell;
}
public void SetCell(string cell, string value)
{
_cellValues[cell] = value;
}
public void SetColumnWidth(int column, double width)
{
_columnWidths[column] = width;
}
private sealed record SpreadsheetSnapshot(
Dictionary<string, string> CellValues,
Dictionary<int, double> ColumnWidths,
string SelectedCell);
}
Notice the internal access modifier on the memento's constructor and property. This prevents code outside the assembly from constructing or inspecting mementos directly. The caretaker stores and returns mementos, but it cannot see inside them. This aligns with the encapsulation principle that the facade pattern uses to hide complexity behind a simple interface.
The caretaker becomes trivially simple:
using System;
using System.Collections.Generic;
public sealed class SpreadsheetCaretaker
{
private readonly Stack<SpreadsheetMemento> _history
= new();
public void Save(SpreadsheetMemento memento)
{
_history.Push(memento);
}
public SpreadsheetMemento Undo()
{
return _history.Count > 0
? _history.Pop()
: null;
}
}
This is a memento pattern best practice in C# that pays off whenever the originator's state structure changes. The caretaker never needs updating because it never knew the state's shape in the first place.
Consider Deep vs Shallow Copies
Choosing between deep and shallow copies is one of the most consequential memento pattern decisions in C#. A shallow copy duplicates the originator's top-level fields but shares references to nested objects. A deep copy duplicates everything recursively. Getting this wrong means either corrupted snapshots or wasted memory.
Here's when each approach is appropriate:
Shallow copies work when:
- All state is composed of value types (
int,decimal,bool,struct) - String properties (strings are immutable in C#, so sharing references is safe)
- The originator doesn't contain collections or nested objects
Deep copies are required when:
- State includes mutable collections (
List<T>,Dictionary<TKey, TValue>) - State includes references to other mutable objects
- The originator's internal objects can change after the snapshot is taken
Here's a practical example showing both approaches:
using System;
using System.Collections.Generic;
using System.Linq;
public sealed class CanvasMemento
{
public IReadOnlyList<ShapeSnapshot> Shapes { get; }
public string BackgroundColor { get; }
public CanvasMemento(
IReadOnlyList<ShapeSnapshot> shapes,
string backgroundColor)
{
Shapes = shapes;
BackgroundColor = backgroundColor;
}
}
public sealed record ShapeSnapshot(
string Type,
double X,
double Y,
double Width,
double Height,
string Color);
public sealed class Canvas
{
private readonly List<Shape> _shapes = new();
private string _backgroundColor = "White";
public CanvasMemento CreateMemento()
{
List<ShapeSnapshot> shapesCopy = _shapes
.Select(s => new ShapeSnapshot(
s.Type, s.X, s.Y,
s.Width, s.Height, s.Color))
.ToList();
return new CanvasMemento(
shapesCopy,
_backgroundColor);
}
public void RestoreMemento(CanvasMemento memento)
{
ArgumentNullException.ThrowIfNull(memento);
_shapes.Clear();
foreach (ShapeSnapshot snapshot in memento.Shapes)
{
_shapes.Add(new Shape
{
Type = snapshot.Type,
X = snapshot.X,
Y = snapshot.Y,
Width = snapshot.Width,
Height = snapshot.Height,
Color = snapshot.Color,
});
}
_backgroundColor = memento.BackgroundColor;
}
public void AddShape(Shape shape)
{
_shapes.Add(shape);
}
}
public sealed class Shape
{
public string Type { get; set; }
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public string Color { get; set; }
}
The CreateMemento method projects each mutable Shape into an immutable ShapeSnapshot record. The _backgroundColor string is safely shared because strings are immutable. This targeted approach -- deep copy for mutable state, shallow copy for immutable state -- balances correctness with efficiency.
Using immutable snapshot types like records eliminates an entire class of bugs. If the snapshot type itself is immutable, it doesn't matter whether you share references to it. This is a memento pattern best practice in C# that makes deep-vs-shallow decisions simpler because immutable data can always be shared safely.
Test State Preservation Thoroughly
Testing memento pattern implementations requires verifying that every piece of state survives a round-trip through save and restore. Missing even one property means silent data loss that may not surface until a user actually triggers an undo.
Here's a thorough testing approach:
using System;
using System.Collections.Generic;
using Xunit;
public class CanvasMementoTests
{
[Fact]
public void CreateMemento_CapturesAllShapes()
{
// Arrange
var canvas = new Canvas();
canvas.AddShape(new Shape
{
Type = "Rectangle",
X = 10, Y = 20,
Width = 100, Height = 50,
Color = "Blue",
});
canvas.AddShape(new Shape
{
Type = "Circle",
X = 50, Y = 50,
Width = 30, Height = 30,
Color = "Red",
});
// Act
CanvasMemento memento = canvas.CreateMemento();
// Assert
Assert.Equal(2, memento.Shapes.Count);
Assert.Equal("Rectangle", memento.Shapes[0].Type);
Assert.Equal("Circle", memento.Shapes[1].Type);
}
[Fact]
public void RestoreMemento_RestoresExactState()
{
// Arrange
var canvas = new Canvas();
canvas.AddShape(new Shape
{
Type = "Rectangle",
X = 10, Y = 20,
Width = 100, Height = 50,
Color = "Blue",
});
CanvasMemento saved = canvas.CreateMemento();
canvas.AddShape(new Shape
{
Type = "Circle",
X = 50, Y = 50,
Width = 30, Height = 30,
Color = "Red",
});
// Act
canvas.RestoreMemento(saved);
CanvasMemento restored = canvas.CreateMemento();
// Assert
Assert.Single(restored.Shapes);
Assert.Equal("Rectangle", restored.Shapes[0].Type);
Assert.Equal(10, restored.Shapes[0].X);
}
[Fact]
public void CreateMemento_ProducesIndependentSnapshot()
{
// Arrange
var canvas = new Canvas();
canvas.AddShape(new Shape
{
Type = "Rectangle",
X = 0, Y = 0,
Width = 50, Height = 50,
Color = "Green",
});
CanvasMemento memento = canvas.CreateMemento();
// Act -- modify originator after snapshot
canvas.AddShape(new Shape
{
Type = "Triangle",
X = 100, Y = 100,
Width = 25, Height = 25,
Color = "Yellow",
});
// Assert -- memento is unaffected
Assert.Single(memento.Shapes);
Assert.Equal("Rectangle", memento.Shapes[0].Type);
}
}
A few testing guidelines for memento pattern best practices in C#:
- Test snapshot independence. After creating a memento, modify the originator and verify the memento's data hasn't changed. This catches shallow copy bugs.
- Test full round-trip restoration. Save state, mutate the originator extensively, restore, and verify every property matches the original snapshot.
- Test boundary cases. Restore a memento into an originator with more state than the snapshot (e.g., the originator now has five shapes but the memento only had two). Verify extra state is properly cleared.
- Test history bounds. If your caretaker limits history size, verify that the oldest mementos are evicted when the limit is reached.
This testing discipline complements the state pattern testing approach. Both patterns manage state transitions, and both require verifying that transitions don't corrupt data.
Performance Considerations with Large State Objects
When your originator manages large state -- a document with thousands of elements, a game world with hundreds of entities, or an image editor with high-resolution pixel data -- creating full snapshots on every change becomes expensive. Memento pattern performance optimization in C# requires strategies that reduce both time and memory costs without sacrificing correctness.
Incremental (Delta) Mementos
Instead of capturing the entire state, capture only what changed since the last snapshot:
using System;
using System.Collections.Generic;
public sealed class DeltaMemento
{
public IReadOnlyDictionary<string, string> ChangedCells
{
get;
}
public IReadOnlyDictionary<string, string> PreviousValues
{
get;
}
public DeltaMemento(
Dictionary<string, string> changedCells,
Dictionary<string, string> previousValues)
{
ChangedCells =
new Dictionary<string, string>(changedCells);
PreviousValues =
new Dictionary<string, string>(previousValues);
}
}
public sealed class LargeSpreadsheet
{
private readonly Dictionary<string, string> _cells
= new();
private readonly Dictionary<string, string>
_pendingChanges = new();
private readonly Dictionary<string, string>
_previousValues = new();
public void SetCell(string cell, string value)
{
if (_cells.TryGetValue(cell, out string existing))
{
if (!_previousValues.ContainsKey(cell))
{
_previousValues[cell] = existing;
}
}
else
{
if (!_previousValues.ContainsKey(cell))
{
_previousValues[cell] = null;
}
}
_cells[cell] = value;
_pendingChanges[cell] = value;
}
public DeltaMemento CreateMemento()
{
var memento = new DeltaMemento(
new Dictionary<string, string>(
_pendingChanges),
new Dictionary<string, string>(
_previousValues));
_pendingChanges.Clear();
_previousValues.Clear();
return memento;
}
public void RestoreMemento(DeltaMemento memento)
{
ArgumentNullException.ThrowIfNull(memento);
foreach (KeyValuePair<string, string> entry
in memento.PreviousValues)
{
if (entry.Value is null)
{
_cells.Remove(entry.Key);
}
else
{
_cells[entry.Key] = entry.Value;
}
}
}
}
Delta mementos are dramatically smaller than full snapshots when only a few cells change out of thousands. The trade-off is increased complexity -- you can't skip mementos during undo because each delta depends on the previous state. Restoring to an arbitrary point requires applying all deltas in reverse order.
Lazy Snapshot Creation
Defer snapshot creation until the memento is actually needed for undo. Use a dirty flag to track whether state has changed:
using System;
public sealed class LazyOriginator
{
private string _content = "";
private bool _isDirty;
private EditorMemento _lastMemento;
public void SetContent(string content)
{
_content = content;
_isDirty = true;
}
public EditorMemento CreateMementoIfDirty()
{
if (!_isDirty && _lastMemento is not null)
{
return _lastMemento;
}
_lastMemento = new EditorMemento(
_content, 0, "Consolas");
_isDirty = false;
return _lastMemento;
}
}
This avoids creating identical snapshots when the user triggers a save point without actually changing anything. It's a small optimization that adds up in applications with frequent auto-save checkpoints.
Compression for Serialized Mementos
When using serialization-based mementos, compressing the serialized data reduces memory usage at the cost of CPU time:
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Text.Json;
public static class MementoCompression
{
public static byte[] CompressState<T>(T state)
{
string json = JsonSerializer.Serialize(state);
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
using var output = new MemoryStream();
using (var gzip = new GZipStream(
output, CompressionLevel.Fastest))
{
gzip.Write(jsonBytes, 0, jsonBytes.Length);
}
return output.ToArray();
}
public static T DecompressState<T>(byte[] compressed)
{
using var input = new MemoryStream(compressed);
using var gzip = new GZipStream(
input, CompressionMode.Decompress);
using var reader = new StreamReader(gzip);
string json = reader.ReadToEnd();
return JsonSerializer.Deserialize<T>(json);
}
}
Compression is most effective for text-heavy state or state with repeated patterns. For binary or already-compact data, the overhead may not be worthwhile. Always measure with representative data before committing to a compression strategy.
These performance considerations are a critical part of memento pattern best practices in C#. The right strategy depends on your state size, change frequency, and undo access patterns. Delta mementos work well for large states with small changes. Full snapshots with compression work well for medium states that change substantially between checkpoints. Lazy creation works well anywhere you have redundant save points. The approach mirrors how the template method pattern lets you define an algorithm skeleton while deferring specific steps -- here, you define the snapshot strategy skeleton and swap in the implementation that fits your performance profile.
Integrate the Memento Pattern with Dependency Injection
When your originator or caretaker has dependencies -- logging, metrics, persistence -- register them with IServiceCollection so construction stays centralized and testable.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
public interface IStateCaretaker<TMemento>
{
void Save(TMemento memento);
TMemento Undo();
}
public sealed class BoundedCaretaker<TMemento>
: IStateCaretaker<TMemento>
where TMemento : class
{
private readonly LinkedList<TMemento> _history = new();
private readonly int _maxSize;
public BoundedCaretaker(int maxSize = 50)
{
_maxSize = maxSize;
}
public void Save(TMemento memento)
{
if (_history.Count >= _maxSize)
{
_history.RemoveFirst();
}
_history.AddLast(memento);
}
public TMemento Undo()
{
if (_history.Count == 0)
{
return null;
}
TMemento memento = _history.Last.Value;
_history.RemoveLast();
return memento;
}
}
Register the caretaker as a scoped or singleton service depending on your application's lifetime requirements:
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<
IStateCaretaker<EditorMemento>>(
new BoundedCaretaker<EditorMemento>(
maxSize: 100));
This follows the inversion of control principle -- consumers depend on the IStateCaretaker<T> abstraction rather than a concrete caretaker class. Swapping in a different caretaker implementation for testing or for a different storage strategy requires zero changes to consuming code.
Frequently Asked Questions
Should I use a record or a class for my memento type?
Records are ideal when your memento is a straightforward data container with no special validation logic. They provide immutability, value equality, and concise syntax. Use a sealed class when you need constructor validation (guard clauses), computed properties, or want to control the internal visibility of the memento's state so only the originator can read it.
How do I handle undo/redo with the memento pattern?
Maintain two collections -- an undo stack and a redo stack. When the user performs an action, push the current state onto the undo stack and clear the redo stack. When undoing, pop from the undo stack, push the current state onto the redo stack, and restore from the popped memento. Redo reverses the process. Always clear the redo stack after a new action -- otherwise you get paradoxical states where redo replays commands that no longer make contextual sense.
What is the difference between the memento pattern and the command pattern for undo?
The command pattern stores operations and reverses them. The memento pattern stores state snapshots and restores them. Commands are ideal when operations are easily reversible (toggle a flag, swap two values). Mementos are better when state changes are complex or non-deterministic -- it's simpler to restore a complete snapshot than to reverse-engineer the exact inverse of a complicated mutation.
How much memory do mementos typically consume?
It depends entirely on the originator's state size and how many mementos you keep. A text editor memento holding a 10KB document string consumes roughly 10KB per snapshot. If you keep 100 snapshots, that's about 1MB -- negligible for most applications. For large state objects, use delta mementos or compression to reduce memory usage. Profile your application with realistic data rather than guessing.
Can the memento pattern be used with the strategy pattern?
Yes. You can use the strategy pattern to select different memento creation strategies at runtime. For example, one strategy might create full snapshots while another creates delta mementos. The originator delegates to the active strategy, and the caretaker doesn't need to know which approach is being used. This is useful when different application contexts have different performance or memory requirements.
How do I prevent external code from modifying a memento?
Use access modifiers to restrict the memento's internal data. Make the memento class sealed and mark its properties with get-only accessors. In C#, you can use the internal modifier on the constructor so only code within the same assembly can create mementos. For additional protection, nest the memento class inside the originator so it's completely inaccessible from outside. The decorator pattern approach of wrapping behavior can also apply here -- wrap a memento in a read-only facade if you need to expose it across assembly boundaries without exposing its internals.
When should I avoid the memento pattern entirely?
Avoid it when the originator's state is trivially small or changes infrequently enough that undo isn't valuable. Also avoid it if the originator's state includes unserializable resources like file handles, database connections, or thread locks -- these cannot be meaningfully captured in a snapshot. In those cases, consider the command pattern with explicit inverse operations instead, or redesign the state to separate serializable data from non-serializable resources.
Wrapping Up Memento Pattern Best Practices
Applying these memento pattern best practices in C# will help you build state management systems that remain clean, testable, and memory-efficient as your application grows. The core themes are consistent: keep mementos immutable, limit history size, encapsulate memento creation inside the originator, choose the right copy depth, and test round-trip state preservation thoroughly.
The memento pattern works best when you treat snapshots as opaque, frozen artifacts that the caretaker stores but never inspects. When the originator owns all the logic for creating and interpreting mementos, your code stays decoupled and resilient to internal state changes. Combine that with bounded history, serialization for complex state, and performance-aware strategies like delta mementos or compression, and you have a state management foundation that handles everything from simple form undo to complex document editing.
Remember that best practices are guidelines informed by production experience, not rigid mandates. Start with the simplest approach -- full immutable snapshots with a bounded history -- and introduce delta mementos or compression only when profiling reveals a need. The goal isn't maximum sophistication -- it's a codebase where state snapshots are reliable, memory usage is predictable, and undo always works exactly as the user expects.

