Event Store
Guide to designing event stores for event-sourced applications — covering event schemas, projections, snapshotting, and CQRS integration.
When to Use This Skill
- - Designing event sourcing infrastructure
- Choosing between event store technologies
- Implementing custom event stores
- Building projections from event streams
- Adding snapshotting for aggregate performance
- Integrating CQRS with event sourcing
Core Concepts
Event Store Architecture
CODEBLOCK0
Event Store Requirements
| Requirement | Description |
|---|
| Append-only | Events are immutable, only appends |
| Ordered |
Per-stream and global ordering |
|
Versioned | Optimistic concurrency control |
|
Subscriptions | Real-time event notifications |
|
Idempotent | Handle duplicate writes safely |
Technology Comparison
| Technology | Best For | Limitations |
|---|
| EventStoreDB | Pure event sourcing | Single-purpose |
| PostgreSQL |
Existing Postgres stack | Manual implementation |
|
Kafka | High-throughput streams | Not ideal for per-stream queries |
|
DynamoDB | Serverless, AWS-native | Query limitations |
Event Schema Design
Events are the source of truth. Well-designed schemas ensure long-term evolvability.
Event Envelope Structure
CODEBLOCK1
Schema Evolution Rules
- 1. Add fields freely — new optional fields are always safe
- Never remove or rename fields — introduce a new event type instead
- Version event types —
OrderPlacedV2 when the schema changes materially - Upcast on read — transform old versions to the current shape in the deserializer
PostgreSQL Event Store Schema
CODEBLOCK2
Event Store Implementation
CODEBLOCK3
Projections
Projections build read-optimised views by replaying events. They are the "Q" side of CQRS.
Projection Lifecycle
- 1. Start from checkpoint — resume from last processed global position
- Apply events — update the read model for each relevant event type
- Save checkpoint — persist the new position atomically with the read model
Projection Example
CODEBLOCK4
Projection Design Rules
- - Idempotent handlers — replaying the same event twice must not corrupt state
- One projection per read model — keep projections focused
- Rebuild from scratch — projections should be deletable and fully replayable
- Separate storage — projections can live in different databases (Postgres, Elasticsearch, Redis)
Snapshotting
Snapshots accelerate aggregate rehydration by caching state at a known version.
Use when streams exceed ~100 events, aggregates have expensive rehydration, or on a cadence (e.g., every 50 events).
Snapshot Flow
CODEBLOCK5
CQRS Integration
CQRS separates the write model (commands → events) from the read model (projections).
CODEBLOCK6
Key Principles
- 1. Write side validates commands, emits events, enforces invariants
- Read side subscribes to events, builds optimised query models
- Eventual consistency — reads may lag behind writes by milliseconds to seconds
- Independent scaling — scale reads and writes separately
Command Handler Pattern
CODEBLOCK7
EventStoreDB Integration
CODEBLOCK8
DynamoDB Event Store
CODEBLOCK9
DynamoDB table design: PK=STREAM#{id}, SK=VERSION#{version}, GSI1 for global ordering.
Best Practices
Do
- - Name streams
{Type}-{id} — e.g., INLINECODE4 - Include correlation / causation IDs in metadata for tracing
- Version event schemas from day one — plan for evolution
- Implement idempotent writes — use event IDs for deduplication
- Index for your query patterns — stream, global position, event type
Don't
- - Mutate or delete events — they are immutable facts
- Store large payloads — keep events small; reference blobs externally
- Skip optimistic concurrency — prevents data corruption
- Ignore backpressure — handle slow consumers gracefully
- Couple projections to the write model — projections should be independently deployable
NEVER Do
- - NEVER update or delete events — Events are immutable historical facts; create compensating events instead
- NEVER skip version checks on append — Optimistic concurrency prevents lost updates and corruption
- NEVER embed large blobs in events — Store blobs externally, reference by ID in the event
- NEVER use random UUIDs for event IDs without idempotency checks — Retries create duplicates
- NEVER read projections for command validation — Use the event stream as the source of truth
- NEVER couple projections to the write transaction — Projections must be rebuildable independently