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.
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.
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.
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.
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.
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
Commands change state and return nothing meaningful; queries read state and cause no change. CQRS gives each its own model.
An append-only, immutable log of domain events that is the system's source of truth.
A read model built (and rebuildable) by applying events into a query-optimized view.
Reconstructing state by applying events in order from the start: state = reduce(apply, events).
Read models catch up to writes asynchronously, so a query may briefly miss the latest command.
Because every change is an event, you get a complete history and can reconstruct any past state.
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) {} }45// --- 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 state9 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)); // immutable12 }13}1415// --- 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.
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
1The Financial Ledger That Needs a Perfect Audit Trail
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.
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.
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
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).
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.
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.