One of the challenges I faced while developing a Sudoku game was managing undoable, persistent game state in a clean, extensible way. This blog post walks through how I designed my game state system using the Memento Pattern, clean abstractions, and dual in-memory and Azure Blob-based implementations.
The Goal
I needed to:
- Track the game board state as players made moves.
- Provide undo functionality.
- Support both ephemeral (in-memory) and durable (Azure Blob) state storage.
- Keep the architecture clean and testable.
GameStateMemento
At the core of the architecture is the GameStateMemento
class — a snapshot of the game state at a point in time:
public class GameStateMemento
{
public GameStateMemento(string puzzleId, Cell[] board, int score)
{
PuzzleId = puzzleId;
Board = board;
Score = score;
LastUpdated = DateTime.UtcNow;
}
public string PuzzleId { get; init; }
public Cell[] Board { get; private set; }
public int Score { get; private set; }
public DateTime LastUpdated { get; init; }
}
It represents the full state needed to restore a puzzle.
IGameStateManager Interface
This abstraction allows my app to plug in different backing stores without changing how game logic interacts with state:
public interface IGameStateManager
{
GameStateMemoryType MemoryType { get; }
Task DeleteAsync(string puzzleId);
Task<GameStateMemento?> LoadAsync(string puzzleId);
Task SaveAsync(GameStateMemento gameState);
Task<GameStateMemento?> UndoAsync(string puzzleId);
}
This lets me easily swap between in-memory and persistent implementations.
In-Memory Storage Implementation with Circular Undo
The in-memory game state manager uses a circular stack to manage the game state. This class is used mostly for generating new puzzles, but could easily be used for offline game play.
public class InMemoryGameStateManager : IGameStateManager
{
private readonly CircularStack<GameStateMemory> _gameState = new(50);
public GameStateMemoryType MemoryType => GameStateMemoryType.InMemory;
public Task DeleteAsync(string puzzleId)
{
... // code hidden for brevity
}
public Task<GameStateMemory> LoadAsync(string puzzleId)
{
...
}
public Task SaveAsync(GameStateMemory gameState)
{
...
}
public Task<GameStateMemory> UndoAsync(string puzzleId)
{
...
}
}
To support multiple undos while avoiding memory bloat, I used a bounded circular stack:
public class CircularStack<T>
{
private readonly T[] _buffer;
private int _top;
public int Capacity { get; }
public int Count { get; private set; }
public CircularStack(int capacity)
{
Capacity = capacity;
_buffer = new T[capacity];
_top = -1;
}
public void Push(T item)
{
_top = (_top + 1) % Capacity;
_buffer[_top] = item;
if (Count < Capacity) Count++;
}
public T Pop()
{
var item = _buffer[_top];
_top = (_top - 1 + Capacity) % Capacity;
Count--;
return item;
}
public T Peek() => _buffer[_top];
public void Clear() => Count = 0;
}
This keeps the last N (e.g., 50) game states available for undo, using a fixed-size buffer.
Persistent Storage
So I chose Azure Blob storage over Redis cache mostly for costs. Since this is just a demo app, I can tolerate slower storage performance in exchange for lower cost. If your application requires lower-latency persistence, Redis may be a better fit. Should your needs change, Redis can easily be swapped in for Azure Blob storage via the shared IGameStateManager interface.
public class AzureStorageGameStateManager(IStorageService storageService) : IGameStateManager, IDisposable
{
public GameStateMemoryType MemoryType => GameStateMemoryType.AzureBlobPersistence;
... // code hidden for brevity
}
Undo
is handled by deleting the latest blob and loading the previous one. A semaphore ensures safe concurrent access in multi-threaded environments.
public async Task<GameStateMemory> UndoAsync(string puzzleId)
{
await _semaphore.WaitAsync();
try
{
var blobList = await GetSortedBlobNamesAsync(puzzleId);
if (blobList.Count == 0)
{
return null;
}
var latestBlobName = blobList.Last();
await storageService.DeleteAsync(ContainerName, latestBlobName);
blobList.RemoveAt(blobList.Count - 1);
if (blobList.Count == 0)
{
return null;
}
var previousBlobName = blobList.Last();
return await storageService.LoadAsync<GameStateMemory>(ContainerName, previousBlobName);
}
finally
{
_semaphore.Release();
}
}
TrimHistoryIfNeededAsync
ensures that the number of states doesn't go beyond a given threshold. This just ensures the persistent storage behaves the same as the in-memory storage.
private async Task TrimHistoryIfNeededAsync(string puzzleId)
{
var blobs = await GetSortedBlobNamesAsync(puzzleId);
if (blobs.Count > MaxUndoHistory)
{
var excess = blobs.Count - MaxUndoHistory;
var oldest = blobs.Take(excess);
foreach (var blobName in oldest)
{
await storageService.DeleteAsync(ContainerName, blobName);
}
}
}
Switching Memory Types
Because of the shared IGameStateManager
interface, components like my puzzle solver or game controller can simply ask for the memory type they want:
services.AddSingleton<Func<string, IGameStateManager>>(sp => key =>
{
return key switch
{
GameStateTypes.InMemory => sp.GetRequiredService<InMemoryGameStateManager>(),
GameStateTypes.AzurePersistent => sp.GetRequiredService<AzureStorageGameStateManager>(),
_ => throw new ArgumentException($"Unknown game state memory type: {key}")
};
});
public class PuzzleSolver(IEnumerable<SolverStrategy> strategies, Func<string, IGameStateManager> gameStateMemoryFactory) : IPuzzleSolver
{
private readonly IGameStateManager _gameStateMemory = gameStateMemoryFactory(GameStateTypes.InMemory);
... // code hidden for brevity
}
Using DI and named instances, I can route the correct implementation based on the situation (generating a puzzle vs. playing a game).
Why This Design Works
✅ Undo Support: Both memory types support it via versioning or stack.
✅ Swappable: Storage is abstracted away, enabling testing and expansion (e.g., LiteDB, SQLite).
✅ Performant: In-memory is fast, while Blob is scalable for cloud users.
✅ Extendable: Future ideas like auto-saving, time-travel debugging, or multiplayer replay become possible.
Next Steps
I plan to:
- Add encryption to blob-stored states.
- Track more metadata (e.g., elapsed time, move history).
If you're building games, puzzle engines, or any app requiring time-travel debugging or undoable state, consider combining the Memento pattern with bounded stacks and cloud storage abstractions like this.
Have you built a system with undoable state or used similar design patterns? I’d love to hear how you approached it. Drop a comment or send me your thoughts — especially if you have ideas for improving this architecture further.