CQRS and Event Sourcing
This guide introduces the patterns underlying Angzarr. If you're already familiar with CQRS/ES, skip to the Components.
Learning Resources
Visual Guides
- EDA Visuals by @boyney123 — Bite-sized diagrams explaining EDA concepts (PDF)
Talks
- CQRS and Event Sourcing — Greg Young's foundational talk (2014)
- Event Sourcing You are doing it wrong — David Schmitz on common pitfalls (2018)
- A Decade of DDD, CQRS, Event Sourcing — Greg Young retrospective (2016)
Articles
- Event Sourcing pattern — Microsoft Azure Architecture
- CQRS pattern — Microsoft Azure Architecture
- Event Sourcing — Martin Fowler
- CQRS — Martin Fowler
Glossary
Core Concepts
| Term | Definition |
|---|---|
| Aggregate | A cluster of domain objects treated as a single unit. Has a root entity that controls access. |
| Aggregate Root | The entry point entity. All external references go through the root. Identified by UUID. |
| Command | A request to change state. Imperative mood: CreateCustomer, ReserveFunds. May be rejected. |
| Event | An immutable fact that something happened. Past tense: CustomerCreated, FundsReserved. Cannot be rejected. |
| Event Store | Append-only database of events. The source of truth. Events are never modified or deleted. |
| Event Sourcing | Storing state as a sequence of events. Current state is derived by replaying events. |
| CQRS | Command Query Responsibility Segregation. Separate models for reading and writing. |
Angzarr Components
| Term | Definition |
|---|---|
| Domain | A bounded context representing a business capability (e.g., "player", "table", "hand"). Each domain has exactly one aggregate codebase. |
| Projector | Service that subscribes to events and performs side effects—building read models, writing to databases, streaming to clients. |
| Saga | Service that subscribes to events and emits commands to other aggregates. Orchestrates multi-domain workflows. |
| Process Manager | Stateful saga that tracks workflow state using correlation IDs. |
| Snapshot | Cached aggregate state at a point in time. Optimization to avoid replaying all events. |
Event Sourcing Explained
Traditional State Storage
┌─────────────────────────────────────┐
│ players table │
├─────────────────────────────────────┤
│ id: "player-123" │
│ username: "Alice" │
│ bankroll: 1500 │
│ updated_at: 2024-01-15 10:30:00 │
└─────────────────────────────────────┘
Problem: We only know current state.
- When did Alice register?
- How did she accumulate 1500?
- What was her bankroll before the last change?
Event Sourced Storage
┌─────────────────────────────────────────────────────────────┐
│ events table │
├──────┬───────────────────────────┬──────────────────────────┤
│ seq │ type │ data │
├──────┼───────────────────────────┼──────────────────────────┤
│ 0 │ PlayerRegistered │ {username: "Alice", ...} │
│ 1 │ FundsDeposited │ {amount: 1000, ...} │
│ 2 │ FundsReserved │ {amount: 500, ...} │
│ 3 │ FundsReleased │ {amount: 500, ...} │
│ 4 │ FundsDeposited │ {amount: 500, ...} │
└──────┴───────────────────────────┴──────────────────────────┘
Current state: replay events 0-4 → bankroll = 1000 - 500 + 500 + 500 = 1500
Benefits:
- Complete audit trail
- Time travel (state at any point)
- Debug by replaying
- Never lose information
State Reconstruction
def rebuild_state(events):
state = empty_state()
for event in events:
state = apply(state, event)
return state
With snapshots (optimization):
def rebuild_state(events, snapshot):
if snapshot:
state = snapshot.state
events = events[snapshot.sequence + 1:]
else:
state = empty_state()
for event in events:
state = apply(state, event)
return state
Idempotent State Reconstruction
Events can contain either deltas (changes) or absolute values (facts). Both are valid—sequence numbers make either approach idempotent.
Delta approach:
# FundsReserved { amount: 500 } # Delta: "reserve 500 more"
def apply(state, event, sequence):
if sequence <= state.last_applied:
return # Already applied—skip
state.reserved += event.amount
state.last_applied = sequence
Absolute value approach:
# FundsReserved { amount: 500, new_reserved: 500, new_available: 500 }
def apply(state, event):
state.reserved = event.new_reserved # Idempotent by design
state.available = event.new_available
Trade-offs:
| Approach | Pros | Cons |
|---|---|---|
| Deltas | Smaller events, clearer intent | Requires sequence tracking |
| Absolute values | Idempotent by design, self-describing | Larger events, computed at write time |
⍼ Angzarr's sequence numbers ensure events are never applied twice, making deltas safe. Choose based on your domain's needs.
CQRS Explained
CQRS separates the write model (commands/events) from read models (projections):
Why separate?
- Write model optimized for consistency (validate, sequence events)
- Read models optimized for specific query patterns
- Scale reads independently from writes
- Each projection can use the best storage for its purpose
Command Flow in Angzarr
Example: Player Bankroll
Commands (Requests)
RegisterPlayer { username: "Alice", initial_bankroll: 1000 }
DepositFunds { amount: 500 }
ReserveFunds { amount: 200, table_id: "table-1" }
Events (Facts)
PlayerRegistered { username: "Alice", initial_bankroll: 1000 }
FundsDeposited { amount: 500, new_bankroll: 1500 }
FundsReserved { amount: 200, new_available: 1300, new_reserved: 200 }
State (Derived)
PlayerState {
username: "Alice",
bankroll: 1500, # Total funds
reserved: 200, # Locked for tables
available: 1300, # bankroll - reserved
}
Validation Rules
| Command | Precondition | Validation |
|---|---|---|
| RegisterPlayer | Player must not exist | Username required |
| DepositFunds | Player must exist | Amount > 0 |
| ReserveFunds | Player must exist | Amount > 0, available >= amount |
Event Design Philosophy
Internal vs External Events
Some frameworks distinguish between "internal" events (implementation details) and "external" events (public API contracts). ⍼ Angzarr rejects this classification at the event level.
Events are immutable data. Classification as "internal" or "external" is a usage decision, not an intrinsic property:
| Approach | Problem |
|---|---|
| Mark events as internal/external | Classification is frozen. Can't later decide to expose an "internal" event without schema changes. |
| Separate internal/external event stores | Duplication, synchronization complexity, defeats event sourcing benefits. |
⍼ Angzarr's approach: All events are stored as data. What you do with them determines visibility:
- Internal use: Sagas and projectors within your system consume events directly
- External publication: A projector at the boundary transforms and publishes events to external systems
Internal: Aggregate → Event Store → Saga → Aggregate
External: Aggregate → Event Store → Projector → External API/CloudEvents/Webhook
The projector is the boundary. It can:
- Filter which events to publish externally
- Transform event schemas for external consumers
- Add external envelope formats (CloudEvents, etc.)
- Version external contracts independently from internal events
This preserves immutability while giving you full control over external contracts.
Next Steps
- Aggregates — Command handling and event emission
- Sagas — Cross-domain workflows
- Why Poker — Why the poker domain exercises every pattern