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.
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.
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.
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.
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.
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.
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 cadence | every 100-1000 events or every hour, whichever comes first |
| Kafka retention for the log | months to forever — this is the source of truth |
| Projections that fit before it hurts | 5-10 read models per event stream, more and you're operationally overloaded |
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.