Reads and writes often have opposite needs. CQRS separates them; event sourcing stores changes as a log of events. Powerful tools — and easy to misapply.
Why: the write side (commands that change state and enforce rules) and the read side (queries optimized for display) often have conflicting needs, so CQRS separates them into different models — letting each be optimized and scaled independently. When: use it where read and write loads or shapes differ sharply; it adds complexity, so do not apply it everywhere. Where: it pairs naturally with DDD (commands act on aggregates) and event-driven systems (reads are projections).
Traditional: one model does both reads and writes (fine for most apps).
CQRS: separate models, optimized independently:
COMMAND side validates + changes state via the domain model (aggregates)
│ emits events / updates
▼
QUERY side denormalized read models built for fast display
Benefit: scale + optimize each side separately.
Cost: two models + eventual consistency. Don't use it by default.Why: once reads and writes are separate models, the read side usually lags the write side by a moment (it is updated asynchronously) — this eventual consistency is the price of the scalability, and the UI and users must tolerate it. When: accept it when a brief staleness is fine (most feeds, dashboards); avoid CQRS where strong read-after-write consistency is mandatory. Where: this same trade-off recurs across distributed systems (the CAP theorem in the Architecture course).
write ──► command model (source of truth, immediately consistent)
│ publishes change
▼
read model (updated asynchronously — briefly STALE)
"I posted a comment but don't see it yet" = eventual consistency.
Design the UX for it (optimistic UI, "pending" states) — or don't use CQRS.
Strong read-after-write required? CQRS is the wrong tool.Why: instead of storing current state and overwriting it, event sourcing stores the full sequence of events that happened, and current state is derived by replaying them — giving a perfect audit log, time-travel, and the ability to build new read models from history. When: use it where history and auditability are first-class requirements (finance, ledgers); it is powerful but demanding. Where: it is the Command pattern at data scale, and it feeds CQRS read projections.
// Instead of a mutable balance, store the events that changed it:
type Event =
| { type: 'Deposited'; cents: number }
| { type: 'Withdrawn'; cents: number }
// Current state is a fold over the event log — fully reconstructable:
function balance(events: Event[]): number {
return events.reduce((total, e) =>
e.type === 'Deposited' ? total + e.cents : total - e.cents, 0)
}
// Perfect audit trail + time travel. Cost: complexity, versioning, replay.
// Reach for it when history IS a requirement — not by default.