What is Event Sourcing?
In a traditional system, the database stores the current state: “Account balance = $500.” In event sourcing, the database stores the history of events that led to the current state: “AccountCreated → Deposited($300) → Deposited($400) → Withdrawn($200).” The current state is derived by replaying the event log. This provides a complete audit trail, enables temporal queries (“what was the balance on March 1?”), and makes it easy to build new projections from historical data.
Core Concepts
- Event: an immutable record of something that happened. Never mutated or deleted. Events are the source of truth.
- Aggregate: a domain object (e.g., BankAccount, Order) whose state is derived by replaying its events.
- Event Store: append-only storage for events, ordered per aggregate by sequence number.
- Projection (Read Model): a materialized view derived from the event stream for efficient querying.
- Command: a request to change state (DepositFunds, PlaceOrder). Commands produce events.
Data Model
Event(event_id UUID, aggregate_type VARCHAR, aggregate_id UUID,
event_type VARCHAR, payload JSONB, sequence_number BIGINT,
created_at TIMESTAMP, metadata JSONB)
-- UNIQUE constraint on (aggregate_id, sequence_number) prevents concurrent writes
Snapshot(aggregate_id UUID, aggregate_type, sequence_number,
state JSONB, created_at)
-- Optimization: avoid replaying all events from the beginning
Command Handler Pattern
def handle_deposit(account_id, amount):
# 1. Load current state by replaying events
events = event_store.load(aggregate_id=account_id)
account = BankAccount()
for event in events:
account.apply(event) # mutate state based on event type
# 2. Validate the command
if amount <= 0: raise ValueError("Amount must be positive")
# 3. Produce new event
new_event = DepositedEvent(account_id=account_id, amount=amount,
sequence_number=len(events) + 1)
# 4. Append to event store (optimistic concurrency via sequence_number)
event_store.append(new_event, expected_version=len(events))
# 5. Publish to event bus for projections
event_bus.publish(new_event)
Optimistic Concurrency Control
Two concurrent commands on the same aggregate must not overwrite each other. Solution: the UNIQUE constraint on (aggregate_id, sequence_number) prevents two writes at the same sequence position. The command handler reads the current version (N), writes at version N+1. If another command already wrote N+1, the INSERT fails with a unique constraint violation — retry with the latest version. This is optimistic locking without explicit locks.
Projections and Read Models
The event store is normalized but inefficient for queries. Projections consume the event stream and build read-optimized views:
class AccountBalanceProjection:
def on_event(self, event):
if event.type == 'Deposited':
db.execute("UPDATE account_balances SET balance = balance + %s
WHERE account_id = %s", (event.amount, event.account_id))
elif event.type == 'Withdrawn':
db.execute("UPDATE account_balances SET balance = balance - %s
WHERE account_id = %s", (event.amount, event.account_id))
Projections can be rebuilt from scratch by replaying all events. This enables: fixing bugs in projection logic, creating new read models for new features, auditing — all without touching the source of truth.
Snapshots
An aggregate with 10,000 events takes 10,000 replays to reconstruct current state. Snapshots periodically save the materialized state: every 100 events, serialize the current aggregate state as a Snapshot. On load: find the latest snapshot (if any), load state from snapshot, then replay only events after the snapshot’s sequence_number. This reduces replay cost from O(all events) to O(events since last snapshot).
When to Use Event Sourcing
- Audit trail is a hard requirement (financial, compliance, legal)
- Need temporal queries (“what was the state at time T?”)
- Need to rebuild projections when requirements change
- Complex domain logic where events carry business meaning
- Avoid when: simple CRUD, small team, read-heavy (projections add complexity)
Coinbase system design covers event sourcing for financial audit trails. See common questions for Coinbase interview: event sourcing and financial audit system design.
Stripe system design covers event sourcing for payment processing. Review patterns for Stripe interview: event sourcing and payment system design.
Databricks system design covers event-driven data architectures. See design patterns for Databricks interview: event sourcing and data architecture design.
See also: Atlassian Interview Guide
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Anthropic Interview Guide 2026: Process, Questions, and AI Safety