Blueprint
Fundamentals

Event sourcing

Store the changes, not the state — a design decision that unlocks audit, time travel and rebuilds, and immediately makes every query harder.

01

What it is

Instead of persisting the current state of an entity (order.status = 'shipped'), you persist the immutable sequence of events that produced it ({order_created}, {payment_captured}, {shipped}). The current state is a projection — a function fold over the event log. The event log is the source of truth; everything else is derived and rebuildable.

The pattern shows up in the interview room whenever the domain has strong audit or reconciliation needs (payments, ledgers, inventory), or when a single write must fan out to many derived views (feeds, search indexes, analytics). Interviewers score you on knowing when it earns its cost and when it doesn't.

Say this out loud

I'd event-source anything the finance team wants to reconcile later — payments, orders, wallets. Everywhere else I'd keep state-first with an event log as an outbox, not as the primary store.

03

The variants and when to pick each

Pure event sourcing — the log is the database

Every state-changing command produces one or more events, appended to a per-aggregate stream. The current state is rebuilt by replaying events on demand (or from a periodic snapshot). Reads and writes go through different paths: writes append to the log, reads consult a projection.

This is the design that lets you answer 'what was the balance at 9:07 pm last Tuesday?' — replay to that timestamp. It's also the design that makes ad-hoc SQL queries painful.

Trade-offYou give up cheap ad-hoc queries — every new question requires either replaying the log through a fold function or building a dedicated projection ahead of time.

State + event log via the transactional outbox

The 90%-of-the-time compromise: keep a normal state table AND write an event row in the same database transaction. A separate shipper reads the event table and publishes to Kafka/SNS. You get the audit log and the fan-out benefits without giving up SQL for your read path.

This pattern is often what people mean when they casually say 'we're doing event sourcing' — it isn't, but it captures most of the value with none of the operational tax.

Trade-offThe state table can drift from the event log if writes bypass the transaction — code hygiene has to enforce that every mutation emits an event.

CQRS — same log, many read models

Command Query Responsibility Segregation splits the write side (commands → events) from the read side (projections tuned to specific queries). One order-events stream might feed: a shipping projection keyed by tracking number, a customer projection listing recent orders, an analytics projection aggregating by day. Each projection is optimal for its query and eventually consistent with the log.

The read models are cheap because they're just consumers — you can add, drop or rebuild them without touching the write path.

Trade-offMultiple read models must all be updated as events flow. A bug in a projection means it's silently stale until you notice or rebuild it from the log.

Snapshots to keep replay bounded

Naively, replaying an aggregate that's been alive for a year means folding every one of its events. The standard solution: periodic snapshots. Every N events (or every T hours), write the current state to a side table. Recovery loads the latest snapshot + replays only the events after it.

The snapshot policy is a knob — more snapshots mean faster load, more storage, more disk writes.

Trade-offSnapshots create a schema-migration headache: if the aggregate shape changes, old snapshots become unreadable and you have to replay from the beginning of time until fresh snapshots exist.
04

Numbers worth memorising

Storage overhead vs state-first~3-10× (raw events are chattier than final state)
Replay throughput~50k events/s per aggregate on modern hardware
Snapshot cadenceevery 100-1000 events or every hour, whichever comes first
Kafka retention for the logmonths to forever — this is the source of truth
Projections that fit before it hurts5-10 read models per event stream, more and you're operationally overloaded
05

Where the interviewer pushes

The classic push: 'this seems complicated — why not just log changes to a change table?' Answer with the specific value: replay after a bug, time-travel queries, immutable audit, cheap fan-out to N projections with no code changes to the writer. If none of these apply to the domain, don't event-source — a change table is fine and cheaper.

Second push: 'what about schema evolution — the shape of an event today isn't the shape you wanted it to be a year ago'. Real problem, and there's no clean answer. Practically: version events ({v: 2, ...}), keep upcaster functions that transform old events into the current shape at read time, and be strict about not breaking wire compatibility. Event streams are forever, so schema hygiene matters more than in a mutable table.

Shows up in

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.