Event Sourcing and CQRS
Implement event sourcing to capture every state change as an immutable event, and CQRS to optimize read and write paths independently. Covers event store design, projections, snapshots, and when these patterns create more problems than they solve.
Event sourcing stores every state change as an immutable event rather than overwriting the current state. Instead of a users table with the latest values, you have an event log: UserCreated, EmailChanged, AccountDeactivated. The current state is derived by replaying events.
CQRS (Command Query Responsibility Segregation) separates the write model (commands that produce events) from the read model (optimized views for queries).
Event Sourcing Fundamentals
Traditional vs Event-Sourced
Traditional:
UPDATE accounts SET balance = balance - 100 WHERE id = 456;
-- Previous balance is lost
Event Sourced:
Event 1: AccountOpened(id=456, balance=1000)
Event 2: MoneyDeposited(id=456, amount=500)
Event 3: MoneyWithdrawn(id=456, amount=100)
-- Current balance: replay events → 1000 + 500 - 100 = 1400
-- Full audit trail preserved
Event Structure
{
"event_id": "evt_abc123",
"aggregate_id": "order_456",
"aggregate_type": "Order",
"event_type": "OrderPlaced",
"version": 1,
"timestamp": "2026-03-04T15:23:47Z",
"data": {
"customer_id": "cust_789",
"items": [{"sku": "BOOT-001", "qty": 1, "price": 129.99}],
"total": 129.99
},
"metadata": {
"user_id": "usr_admin",
"correlation_id": "req_xyz",
"causation_id": "cmd_place_order"
}
}
Event Store
CREATE TABLE events (
event_id UUID PRIMARY KEY,
aggregate_id UUID NOT NULL,
aggregate_type TEXT NOT NULL,
event_type TEXT NOT NULL,
version INTEGER NOT NULL,
data JSONB NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (aggregate_id, version) -- Optimistic concurrency
);
CREATE INDEX idx_events_aggregate ON events (aggregate_id, version);
The UNIQUE (aggregate_id, version) constraint prevents concurrent writes from creating conflicting events for the same aggregate.
CQRS: Separate Read and Write
Command Side (Write) Query Side (Read)
┌──────────┐ ┌──────────────┐
│ Commands │ │ Read Models │
│ (Create, │ → Events → ──▶ │ (Projections)│
│ Update) │ │ Optimized │
└──────────┘ │ for queries │
↓ └──────────────┘
Event Store Read Database
(append-only) (denormalized)
Projections
Projections transform events into read-optimized views:
class OrderSummaryProjection:
def handle(self, event):
if event.type == "OrderPlaced":
db.execute("""
INSERT INTO order_summaries
(order_id, customer_id, total, status, placed_at)
VALUES (%s, %s, %s, 'placed', %s)
""", event.aggregate_id, event.data['customer_id'],
event.data['total'], event.timestamp)
elif event.type == "OrderShipped":
db.execute("""
UPDATE order_summaries
SET status = 'shipped', shipped_at = %s
WHERE order_id = %s
""", event.timestamp, event.aggregate_id)
Snapshots
Replaying thousands of events per request is slow. Snapshots periodically save the current state:
def load_aggregate(aggregate_id):
snapshot = load_latest_snapshot(aggregate_id)
if snapshot:
state = snapshot.state
events = load_events_after(aggregate_id, snapshot.version)
else:
state = initial_state()
events = load_all_events(aggregate_id)
for event in events:
state = apply_event(state, event)
return state
When to Use (and When Not To)
| Use Event Sourcing | Do Not Use Event Sourcing |
|---|---|
| Full audit trail required (finance, healthcare) | Simple CRUD with no audit needs |
| Complex business workflows | Low-write, high-read applications |
| Need to replay/reprocess historical data | Team unfamiliar with the pattern |
| Event-driven architecture already in place | Tight deadlines, MVP phase |
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Events as CRUD wrappers | No business meaning, just “FieldUpdated” | Model domain events, not data changes |
| No snapshots | Replay time grows unbounded | Snapshot every N events |
| Mutable events | Audit trail is meaningless | Events are immutable, always |
| Ignoring eventual consistency | Read models show stale data | Design UI for eventual consistency |
| Event sourcing everything | Massive complexity for simple domains | Use only where audit trail or replay justifies it |
Event sourcing and CQRS are powerful patterns for the right problems. For the wrong problems, they add complexity without proportional benefit. Choose deliberately.