ADR-002: Snapshot Persistence over Event Sourcing
Status: Accepted Date: 2026-04-16
Context
The inventory aggregate accumulates state through domain operations (AddStack, RemoveFromSlot, Move, etc.). This state must be persisted and reloaded reliably. Two dominant patterns exist:
- Snapshot persistence: Serialize the current aggregate state as a single document (JSON). On load, deserialize the document and reconstruct the aggregate directly.
- Event sourcing: Persist every domain event in an append-only event store. On load, replay all events from the beginning (or from a snapshot checkpoint) to reconstruct state.
Options Considered
Option A: Event Sourcing
Append ContainerItemAddedEvent, ContainerItemRemovedEvent, etc. to an event log per aggregate. Reconstruct state by replaying the log.
Pros:
- Full audit trail, every change is traceable.
- Time-travel debugging: replay state at any point in time.
- Natural fit for CQRS read-model projections.
Cons:
- Replay time grows linearly with aggregate history. A chest modified 10,000 times requires replaying 10,000 events on every load.
- Snapshot checkpointing must be added anyway to bound replay time, which partially offsets the simplicity advantage.
- Game inventory aggregates have high write frequency (each item pickup is a mutation) but low auditability requirements; players do not file “item history” disputes.
- Event schema versioning (upcasting) becomes a permanent maintenance burden as the domain model evolves.
- Infrastructure complexity increases significantly (event store, projection workers).
Option B: Snapshot Persistence (selected)
Serialize the full aggregate state as JSON on every save. Load by deserializing the latest snapshot.
Pros:
- Load time is O(1) regardless of aggregate history.
- Schema evolution is handled by the
InventorySnapshotMapperwhich can apply default values for missing fields, no upcasting pipeline required. - Provider-agnostic: the same JSON snapshot works with file storage, SQLite, SQL Server, or PostgreSQL without changing domain code.
- Simpler infrastructure: no event store, no projection workers, no replay logic.
Cons:
- No built-in audit trail. Domain events are raised but discarded after dispatch unless the Outbox persists them (see ADR-003).
- Cannot reconstruct state at an arbitrary past point in time.
Decision
Use snapshot persistence. The InventorySnapshotMapper translates between the domain aggregate and a versioned InventoryAggregateSnapshot DTO. The snapshot is serialized to JSON and stored as a single row/file per aggregate.
The snapshot format is versioned by convention: new fields are nullable or have defaults, old fields are never removed until a major version bump. The mapper enforces this boundary.
Consequences
InventorySnapshotMapperis the single point of schema migration logic. Any new field added to the domain aggregate must have a corresponding nullable or defaulted entry in the snapshot DTO.- Domain events (
ContainerItemAddedEvent, etc.) are published for real-time notification purposes but are not the source of truth for aggregate state. - If a full audit trail is required in the future, an append-only event log can be added alongside snapshot persistence without changing the core domain model.
- Performance: snapshots are bounded in size by the aggregate’s slot count × max item data. A 100-slot inventory snapshot is approximately 10–50 KB. No read amplification concern.