Event sourcing and CQRS: an interactive event log simulator
Event sourcing stores what happened instead of the current state: every change is appended to a log as an event, such as “deposited $100”, and the current state is whatever replaying the events produces. Nothing is ever overwritten, so the full history is kept and any past state can be rebuilt. CQRS, command query responsibility segregation, usually comes with it: writes append events, and reads are served from separate read models built from those events.
The playground keeps a bank account as 120 events. Move back through the log, add events, take snapshots, and compare the account with a statement read model that runs behind.
The account is 120 events, and its state is whatever replaying them gives: nothing else is stored. Loading it replays all 120 events, which grows with the account’s age; snapshots cap that.
- 113 earlier events
- 114Withdrew $79.07
- 115Withdrew $60.54
- 116Withdrew $195.80
- 117Withdrew $21.24
- 118Withdrew $27.40
- 119Deposited $1,500.00
- 120Withdrew $139.92
How event sourcing works
- Commands express intent, such as “withdraw $50”. The service loads the entity’s current state and checks the rules: is there enough money, is the account frozen.
- Events record what happened, in the past tense, once the command is accepted. They are appended to the entity’s stream and never changed.
- State is rebuilt by folding the events in order, from the start or from a snapshot.
- Projections read the event stream and maintain read models shaped for queries: a statement, a search index, a monthly report.
What you get
- A complete audit trail: why the balance is what it is, entry by entry, which regulators and support teams ask for.
- Time travel: in the playground, replaying the first 72 events shows the account frozen after a suspected fraud, exactly as it was then.
- New read models from old data: a projection added next year can be built by replaying every event since the beginning.
- Integration: other services subscribe to the same events instead of polling for changes.
What it costs
- Replay time: loading an old, busy entity means replaying all its events. Snapshots of the state every so often fix it: with one every 50 events, loading the account replays 20 events instead of 120.
- Eventually consistent reads: read models lag the log. A statement 30 events behind shows the wrong balance until it catches up, so checks that must be exact, like funds before a withdrawal, read from the event store.
- Events are forever: changing an event’s shape means versioning it and handling every old version, or upcasting them on read.
- Deleting data is hard: an immutable log conflicts with the right to be forgotten; personal data is often encrypted per user so deleting the key erases it.
- Concurrency: two commands on the same entity must not both append against stale state, so appends check the stream’s expected version, a form of optimistic locking.
It suits domains where history is the point: ledgers, orders, bookings, workflows. For a typical CRUD screen it adds complexity without much in return; storing the current state plus a separate audit log is often enough.
Event sourcing in code
type Event =
| { type: 'Deposited'; amount: number }
| { type: 'Withdrew'; amount: number }
| { type: 'Frozen' };
function apply(state: Account, e: Event): Account {
switch (e.type) {
case 'Deposited': return { ...state, balance: state.balance + e.amount };
case 'Withdrew': return { ...state, balance: state.balance - e.amount };
case 'Frozen': return { ...state, frozen: true };
}
}
async function withdraw(id: string, amount: number) {
const { state, version } = await load(id); // latest snapshot + events since
if (state.frozen || state.balance < amount) throw new Error('declined');
// append only if nobody else wrote since we loaded: optimistic concurrency on the stream
await store.append(id, [{ type: 'Withdrew', amount }], { expectedVersion: version });
}In a system design interview
Bring it up where history matters, and name the read side: “The ledger is event-sourced: every movement of money is an append-only entry, and a balance is the sum of its account’s entries, with a snapshot every thousand. Projections build the statement view and the daily reconciliation report from the same stream, a second or so behind; a withdrawal checks the balance against the event store itself.” Expect the follow-ups: how events are versioned, how long a replay takes, and how a projection recovers after a bug.
Read the guide to event sourcing and CQRS, see the transactional outbox for publishing events reliably, or watch a strong candidate design a payment system.
Questions people ask
What is event sourcing?
A way of storing data as an append-only sequence of events describing every change, such as “deposited $100”, instead of storing only the current state. The current state is derived by replaying the events in order, so the complete history is preserved and any past state can be reconstructed.
What is CQRS?
Command query responsibility segregation: using separate models for writes and reads. Commands change state, in event-sourced systems by appending events, while queries are served from read models built specifically for them, often in different databases. It lets each side be optimized independently, at the cost of read models lagging the writes.
Why use snapshots in event sourcing?
Because replaying every event of a long-lived entity on each load gets slow. A snapshot saves the state as of some event, and loading starts from the latest snapshot and replays only the events after it. Snapshots are a cache: they can be thrown away and rebuilt from the events.
Are read models in CQRS eventually consistent?
Usually, yes. Projections update read models after events are appended, so for a short time reads can return older data. Operations that need the exact current state, such as checking a balance before a withdrawal, should load from the event store, while screens and reports can tolerate the lag.
What are the downsides of event sourcing?
More complexity: events must be versioned forever as their shape changes, replays and projections need building and monitoring, read models are eventually consistent, deleting personal data from an immutable log needs techniques such as per-user encryption keys, and developers must think in events rather than rows.
When should you use event sourcing?
When the history of changes is itself valuable: financial ledgers, orders, bookings, inventory movements, workflows and anything needing a strong audit trail or the ability to rebuild state and new views from the past. For simple CRUD data, storing the current state with a separate audit log is usually simpler.
How do you handle concurrent writes in event sourcing?
Each entity’s event stream has a version, the number of events in it. A command loads the state and its version, and appends new events only if the stream is still at that version; if another command appended first, the append fails and the command retries with fresh state. This is optimistic concurrency control.