HT
How Things Work
System Online

CQRS & Event Sourcing

Splitting the write model from the read model, and storing immutable events instead of mutable state — for audit trails, time travel, and independently optimized reads and writes.

How It Works

CQRS separates the model that changes data (commands) from the model that reads it (queries), so each can be optimized and scaled independently. Event Sourcing — its frequent companion — stores the full, immutable sequence of domain events as the source of truth and derives current state by replaying them. Together they unlock perfect audit history, time travel, and purpose-built read models, at the cost of added complexity and eventual consistency.

1
CQRS — Split Reads from Writes

Command Query Responsibility Segregation uses separate models for changing data (commands) and reading data (queries). Writes go through a model that enforces business rules; reads come from a model shaped for the screen. The two can scale, optimize, and even use different databases independently.

2
Event Sourcing — Store the Facts, Not the State

Instead of storing current state and overwriting it, event sourcing stores the full sequence of events that happened (Deposited, Withdrawn). The events are an immutable, append-only log — the single source of truth. Nothing is ever updated in place or deleted.

3
State Is a Fold Over Events

Current state isn't stored — it's derived by replaying (folding) the events from the beginning. balance = events.reduce(apply, 0). Because the log is the truth, you can rebuild the read model at any time, build new projections retroactively, and even time-travel to any past state.

4
Projections & Eventual Consistency

Read models (projections) are built by subscribing to the event stream and updating query-optimized views. Often the projection updates asynchronously, so the read side is eventually consistent with the write side — a deliberate trade for scalability and rich, purpose-built read models.

Key Concepts

↔ïļCommand vs Query

Commands change state and return nothing meaningful; queries read state and cause no change. CQRS gives each its own model.

📜Event Store

An append-only, immutable log of domain events that is the system's source of truth.

🖞ïļProjection

A read model built (and rebuildable) by applying events into a query-optimized view.

🔁Fold / Replay

Reconstructing state by applying events in order from the start: state = reduce(apply, events).

âģEventual Consistency

Read models catch up to writes asynchronously, so a query may briefly miss the latest command.

🕰ïļAudit & Time Travel

Because every change is an event, you get a complete history and can reconstruct any past state.

Commands, events & projections
tsx
1// CQRS: separate the write path (commands) from the read path (queries).
2class DepositCommand { constructor(public accountId: string, public amount: number) {} }
3class GetBalanceQuery { constructor(public accountId: string) {} }
4
5// --- Event sourcing: the command APPENDS an event, never mutates state ---
6class AccountCommandHandler {
7 async handle(cmd: DepositCommand) {
8 const events = await store.load(cmd.accountId); // rebuild current state
9 const balance = events.reduce(apply, 0);
10 if (cmd.amount <= 0) throw new Error("invalid amount");
11 await store.append(cmd.accountId, new Deposited(cmd.amount)); // immutable
12 }
13}
14
15// --- Read side: a projection optimized for queries (could be a separate DB) ---
16function apply(balance: number, e: DomainEvent) {
17 return e.type === "Deposited" ? balance + e.amount : balance - e.amount;
18}
19// State at any time = fold(apply, events). Rebuild it anytime by replaying.
20// Time-travel: fold only events up to a timestamp to see a past state.
ðŸ’Ą
Why This Matters

CQRS and event sourcing power financial systems, audit-heavy domains, and large event-driven backends, and they're advanced design-interview favorites. The key skill is judgment: they add real power (history, scalable reads) but also real complexity, so knowing when they pay off — and when plain CRUD is right — matters as much as knowing how they work.

Common Pitfalls

⚠Applying CQRS/event sourcing to simple CRUD — the complexity vastly outweighs the benefit there.
⚠Forgetting eventual consistency — UIs that assume a read immediately reflects the last write will show stale data.
⚠No plan for schema/event evolution (versioning, upcasting) — old events become unreadable as the model changes.
⚠Letting projections drift from the event log with no rebuild path — read models become untrustworthy.
⚠Conflating CQRS (two models) with event sourcing (events as truth) — you can do either without the other.
Real-World Use Cases

1The Financial Ledger That Needs a Perfect Audit Trail

Scenario

A payments system must answer 'why is this balance what it is?' and 'what was the balance on March 3rd?' — but it only stores the current balance, overwriting it on every transaction. History is lost.

Problem

Storing only current state destroys the 'how we got here'. Reconstructing history or auditing a discrepancy is impossible because past values were overwritten. For finance, that's unacceptable.

Solution

Adopt event sourcing: persist every Deposited/Withdrawn event in an append-only store. The balance becomes a projection (fold of events). Auditors get a complete, immutable history, and any past balance is recovered by replaying events up to that date — time travel for free.

ðŸ’Ą

Takeaway: When history and auditability are first-class requirements (finance, compliance), event sourcing makes the change log the source of truth, so you never lose the 'why' behind the current state.

2Reads and Writes With Opposite Needs

Scenario

An e-commerce backend has a complex write side (orders with strict invariants, low volume) and a read side that's hammered for product listings and dashboards (high volume, denormalized, fast).

Problem

A single normalized model forced both paths to compromise: writes were complicated by read-shaped denormalization, and reads suffered from expensive joins over a write-optimized schema. Scaling one penalized the other.

Solution

Apply CQRS: the write model enforces order invariants and emits events; projections build denormalized read models (e.g. a flat 'order summary' view, even in a separate fast store) updated from those events. Each side is optimized and scaled on its own — accepting eventual consistency on reads.

ðŸ’Ą

Takeaway: CQRS shines when read and write workloads have genuinely different shapes and scale. Separate models let each be optimal — but only adopt it where that asymmetry (or event sourcing's audit needs) justifies the added complexity.

Join the discussion

Comments

?

Loading comments...

Cookie preferences

We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.