Event Sourcing
Event Sourcing stores the system's state as an append-only log of events — every state change is captured as an immutable event, and current state is derived by replaying the log. Instead of storing the current row in `orders`, you store `OrderCreated`, `OrderShipped`, `OrderCancelled` events; the order's current state is computed by folding over the events. This gives an audit trail for free, time-travel queries (what was the state at 3 PM Tuesday?), and the ability to rebuild any read model from the log. The trade-offs: complexity, eventual consistency on reads, and an event schema that must be carefully versioned.
How it works
In a traditional CRUD system, the database stores the current state of each entity. When you update an order's status from pending to shipped, the old pending value is overwritten — history is lost. To query ‘what was this order's status at 3 PM Tuesday?’ you'd need separate audit logs, and they're usually incomplete.
Event Sourcing flips the model. The system's state is an append-only log of events:
event_id | aggregate_id | event_type | payload | timestamp
1 | order-42 | OrderCreated | {items: [...], total} | 10:00
2 | order-42 | PaymentReceived | {amount: 100} | 10:05
3 | order-42 | OrderShipped | {tracking: '...'} | 11:30
4 | order-42 | OrderDelivered | {} | 14:00The order's current state is computed by replaying events 1-4: created, paid, shipped, delivered. The current state is a projection of the log.
Key properties:
- Append-only — events are never modified or deleted (except via compaction, which preserves the latest event per aggregate).
- Immutable — once written, an event never changes.
- Ordered — events have a sequence number; replay must preserve order.
- Named and typed — each event type describes a domain fact (
OrderShipped, notupdate status to 'shipped'). - Source of truth — the log is the truth; projections (read models) are derived.
The aggregate (e.g., an Order) is reconstructed by folding events:
def apply(state, event):
if event.type == 'OrderCreated':
return State(status='pending', items=event.items, total=event.total)
elif event.type == 'PaymentReceived':
return state.with(status='paid')
elif event.type == 'OrderShipped':
return state.with(status='shipped', tracking=event.tracking)
...
current_state = fold(apply, initial_state, events_for_order_42)This is the same pattern Git uses internally (commits are events), and the same pattern financial ledgers have used for centuries (every transaction is recorded).
Benefits of Event Sourcing:
- Perfect audit trail — every state change is recorded with timestamp, who, what, why. Required for finance, healthcare, regulated industries.
- Time-travel queries — replay events up to a point in time to see the state then. ‘What was the account balance on Jan 15 at noon?’
- Rebuildable read models — any projection can be rebuilt from scratch by replaying the log. Schema changes are easy: write a new projection, replay.
- Decoupled read models — different projections serve different query patterns, all from the same log. CQRS reads naturally fall out.
- Bug diagnosis — when a bug occurs, replay the events that led to the bad state and see exactly what happened.
- Event-driven integration — other services can subscribe to the event stream and react ( Saga pattern, pub/sub ).
- No update anomalies — there are no updates, only appends. Optimistic concurrency control is natural: each event references the prior version.
Costs:
- Complexity — versioning events, writing projections, handling replays, dealing with side-effects (e.g., sending an email during replay) is hard.
- Eventual consistency on reads — projections lag the log by milliseconds (or more under load).
- Storage growth — the log grows forever; compaction (keeping only the latest event per aggregate) is needed.
- Schema evolution — events are immutable, but their schema evolves. Versioning, upcasting, and backward compatibility are required.
- Side-effects are hard — during a projection rebuild, you must NOT re-send emails or charge credit cards. Idempotency and outbox patterns are essential.
- Learning curve — developers must think in events, not state.
Events are immutable, but their schema evolves. After 6 months in production, you'll want to add a field to OrderCreated, rename a field, or split one event into two. Patterns: (1) versioned events — OrderCreatedV1, OrderCreatedV2; consumers handle both. (2) upcasters — transform old events to the new schema on read; the log stays raw, but consumers see the new shape. (3) weak schema — Avro/Protobuf with backward-compatible field additions. (4) snapshot and migrate — read all old events, write new ones in a new format. Whatever you choose, plan for evolution from day one. Events are forever; their schemas must evolve gracefully.
Performance: replaying 10,000 events to reconstruct an aggregate's state is slow. Two optimizations:
1. Snapshots — periodically persist the current state as a snapshot. To reconstruct, load the latest snapshot and replay only the events after it. Common snapshot cadence: every 100 or 1,000 events. The snapshot is itself derived from the log (it's not the source of truth — the log is).
2. Compaction — for long-lived aggregates (e.g., a customer over 10 years), keep only the latest event per type per aggregate, or fold old events into a single ‘summary’ event. This reduces log size at the cost of historical fidelity.
Projection rebuilds are similar: for a large projection (e.g., daily_revenue over years), you can either (a) replay all events from the beginning (hours for billions of events) or (b) snapshot the projection periodically and resume from the snapshot.
A common operational pattern: keep the raw event log forever (or for the regulatory retention period), maintain snapshots and projections alongside, and rebuild projections as needed when schemas change. The log is canonical; snapshots and projections are derived and rebuildable.
In an event-sourced system, what is the source of truth, and how is current state derived?
Pick one answer.
You need to rebuild a read-model projection after a schema change. How does Event Sourcing make this possible, and what's the main concern?
Pick one answer.
Engineering mental model
Mental model. Think of Event Sourcing as a deliberate boundary in a system. The boundary exists because something becomes harder to manage when everything is done in one place: latency, scale, failure isolation, consistency, cost, or team ownership. The useful question is not “what does Event Sourcing mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”
Before choosing Event Sourcing, name the workload, the critical user path, the dominant bottleneck, the failure you are trying to absorb, and the trade-off you are willing to accept. If you cannot name those five things, the design is probably premature.
message_id = queue.publish({
"type": "event-sourcing",
"key": resource_id
})
# Consumer must be safe to retry.Back-of-the-envelope reasoning
Numerical lens: write down traffic, payload size, read/write ratio, peak multiplier and durability target before choosing a component. The numbers should justify the architecture.
Interactive thought experiment: Event Sourcing
Change the variables below and predict what breaks first in Event Sourcing. The production lab can later reuse these same inputs.
Change one variable at a time. Predict the failure mode first, then move the slider and see whether your mental model matches the simplified system response.
If you are stuck on Event Sourcing, start by drawing the request path and marking every network hop, stateful component, queue, cache and failure boundary. Then estimate where the system will saturate.
You increase traffic by 10× in a system using Event Sourcing. What should you inspect first?
Pick one answer.
Which statement is the safest engineering habit when using Event Sourcing?
Pick one answer.
You have dashboards for traffic, latency, errors and saturation. You can change the architecture, but every change has operational cost.
Production scenario: your system uses Event Sourcing, traffic suddenly spikes, and p99 latency doubles. What is your first move?
Interview drill
Answer this without notes: When would you choose Event Sourcing, and when would you intentionally avoid it? Mention at least one bottleneck it addresses, one failure mode it introduces, and one alternative. Then quantify the workload you are designing for.
For Event Sourcing, separate producer speed from consumer speed. The key design question is what happens when production temporarily exceeds processing capacity: queue it, shed it, slow producers down, or degrade the feature.
Numerical sanity check
A simple queue sanity check: if producers create 8,000 messages/s and consumers process 6,000 messages/s, backlog grows at roughly 2,000 messages/s until the imbalance is corrected.
Do not optimize for a memorized definition. Reason from the workload and failure mode.
A downstream service slows down while Event Sourcing keeps accepting traffic. Would you rather apply backpressure, queue more work, shed load, or degrade? Explain the trade-off.
Pick one answer.
What you gain, what you pay
- +Perfect audit trail — every state change recorded with full context.
- +Time-travel queries — replay up to any point in time.
- +Rebuildable read models — any projection can be rebuilt from the log.
- +Multiple projections from one log — CQRS reads fall out naturally.
- +Bug diagnosis — replay the events leading to a bad state.
- +Event-driven integration — other services can subscribe to the stream.
- +No update anomalies — only appends, so optimistic concurrency is natural.
- −Complexity — versioning events, writing projections, handling replays.
- −Eventual consistency on reads — projections lag the log.
- −Storage growth — the log grows forever; needs compaction.
- −Schema evolution is hard — events are immutable but their schema evolves.
- −Side-effects are tricky — must not re-trigger them during replay.
- −Steep learning curve — developers must think in events, not state.
How this breaks in production
- Event schema evolves incompatibly — old events can't be replayed with new code; requires upcasters or versioned events.
- Projection lag grows under load — read models fall behind writes.
- Side-effects re-triggered during replay — emails re-sent, cards re-charged; must use outbox or idempotency keys.
- Snapshot corruption — a bad snapshot requires full replay to recover.
- Event ordering bugs — out-of-order events produce wrong state; needs sequencing or version vectors.
- Unbounded log growth — without compaction, storage costs grow forever.
Don't fall into these traps
- •Treating events as CRUD updates — naming events `OrderUpdated` instead of `OrderShipped` loses domain meaning.
- •Storing current state in the event payload — events should describe what happened, not the new state.
- •Not versioning events from day one — schema evolution becomes painful later.
- •Triggering side-effects inside projection logic — replays re-trigger them.
- •Not snapshotting long-lived aggregates — replaying thousands of events per read is slow.
- •Forgetting idempotency — replays and redeliveries cause duplicate effects without it.
Real systems using this
How real systems implement this
- Apache Kafka as an event log — Kafka's design is built around the event log concept — topics are append-only, partitioned logs; consumers are projections. Many CQRS systems use Kafka as the event store, with consumers (Flink, Kafka Streams, custom services) building read models.
- EventStoreDB — A purpose-built event store that implements Event Sourcing natively — append-only streams per aggregate, subscriptions for projections, built-in snapshot support. Used in DDD-heavy systems where events are first-class.
- Axon Framework (Java) — A Java framework that implements CQRS + Event Sourcing explicitly: aggregates apply events, events are persisted to an event store, query-side projections subscribe and build read models. The canonical reference implementation for the pattern.
- Git internals — Git's commit log is event-sourced — every commit is an immutable event, the current tree state is derived by replaying commits. Branches and tags are projections. This is why Git can rebase, cherry-pick, and time-travel.
Practice saying it out loud
- Q1What is Event Sourcing, and how does it differ from traditional CRUD?
- Q2How do you rebuild a read-model projection in an event-sourced system? What are the concerns?
- Q3How do you handle event schema evolution over years in production?
- Q4Why is Event Sourcing popular in financial systems? What does it give you that CRUD doesn't?
- Q5What are the trade-offs of Event Sourcing? When would you NOT use it?
Further reading & references
Core explanations are original NO CAP material. External references are provided for deeper study and standards.
What next?
Mark as understood once the mental model clicks.
Next recommended
CQRS