CQRS
A single data model forced to serve both writes and reads eventually deforms under the pressure - write-side schemas grow denormalized columns for dashboards, read-side queries are slowed by indexes that only exist for constraint enforcement, and neither job gets done well.
Problem
Most applications begin with one model used for everything. The same Order class handles a checkout submission, a seller's dashboard, and a warehouse picking screen. The same database table backs all three. This works until the read and write workloads start pulling the schema in opposite directions. The dashboard wants a denormalized projection with precomputed totals. The warehouse wants full-text search over SKUs. The checkout wants a tight transactional boundary with minimal columns touched. Adding indexes, materialized views, and denormalized columns into one table slowly turns the schema into a compromise that serves no workload well.
The underlying mismatch is that commands (operations that change state) and queries (operations that read state) have fundamentally different requirements - different consistency guarantees, different shapes, different performance characteristics. Forcing them through the same model means every change is a tug-of-war between two stakeholders.
Forces
- Read and write workloads diverge. Writes need small, validated, transactional touchpoints. Reads need wide, denormalized, query-optimized shapes. A shared schema has to pick a side.
- Scaling characteristics differ. Reads typically outnumber writes by 10× to 1000×. A model that optimizes both equally wastes capacity on whichever side is the minority.
- Consistency requirements differ. Writes are usually the authoritative source of truth and need strong consistency. Reads are often tolerant of millisecond-to-second staleness if the trade is faster, richer queries.
- Team ownership differs. The team building a seller-analytics dashboard has different priorities from the team enforcing checkout invariants. A shared model couples their roadmaps.
Solution
Command Query Responsibility Segregation (CQRS) splits the model in two. Commands flow through a write model optimized for state changes; queries flow through one or more read models optimized for the shapes the product actually asks for. The two sides stay synchronized asynchronously - typically by the write side publishing events that read-side projectors consume and apply to their own stores.
Typical shape:
// Write side - normalized, transactional, small
class CreateOrderHandler {
async handle(cmd: CreateOrderCommand): Promise<void> {
await db.transaction(async (tx) => {
const order = await tx.insert(orders).values(cmd.toRow());
await tx.insert(outboxEvents).values({
eventType: 'order.created',
payload: order,
});
});
}
}
// Read side - denormalized, query-optimized, eventually consistent
class OrderListProjection {
async on(event: OrderCreated): Promise<void> {
await elastic.index({
index: 'orders-read',
id: event.orderId,
body: {
orderId: event.orderId,
customerName: event.customer.name,
totalCents: event.total,
itemSkus: event.items.map((i) => i.sku),
createdAt: event.createdAt,
},
});
}
}The write side owns business rules and invariants. The read side owns query ergonomics. Neither side has to compromise for the other.
CQRS is not about databases. It is about admitting that "change what is true" and "describe what is true" are different jobs that deserve different tools.
Read-model choices. The read store does not have to be the same engine as the write store. Common pairings: Postgres writes with Elasticsearch reads (text search); Postgres writes with Redis reads (hot-path lookups); Postgres writes with a warehouse like BigQuery (analytics). Each read model is built for one use case and can be rebuilt by replaying events.
Event transport. The write side must publish change events reliably. Pair CQRS with the Outbox pattern so events are atomic with the write, then have projectors consume them through a broker.
Consistency delay. The lag between write and read visibility is the defining trade of CQRS. In practice it is usually 50ms–2s. The application must be designed to tolerate it - either by reading from the write side for "just-did-this" cases, or by explicitly showing pending state in the UI.
When NOT to Use
- Simple CRUD applications. If your reads and writes have nearly identical shapes and modest volume, one model serves both cheaply. CQRS adds a broker, projectors, two stores, and an eventual-consistency semantic your product does not need.
- Strong read-after-write consistency is required. In domains where a user updating a value must immediately see that value in a list (regulated settings screens, admin consoles, seller price confirmation), CQRS's eventual consistency is a UX hazard. You either read back from the write side (partially defeating the point) or keep a single model.
- Small teams without event-streaming operational maturity. Running projectors reliably - rebuilding them on schema changes, handling poison messages, monitoring lag - is a meaningful operational surface. If the team cannot own it, a single well-indexed database is better.
- Low-volume internal tools. A three-person internal dashboard with a hundred records does not benefit from the complexity. Reach for CQRS when the read workload justifies its own shape, not because the pattern is interesting.
Related Patterns
Outbox is the reliable transport for change events between the write side and read-side projectors - CQRS without Outbox eventually loses events during broker hiccups and drifts out of sync. Event Sourcing is CQRS taken one step further: instead of storing current state on the write side, store the sequence of events that produced it, and treat every read model as a projection over that log. CQRS can be adopted without Event Sourcing; Event Sourcing almost always implies CQRS.
References
- Young, Greg. "CQRS Documents." cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
- Fowler, Martin. "CQRS." martinfowler.com/bliki/CQRS.html
- Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013. Chapter 4 (Architecture).
- Kleppmann, Martin. Designing Data-Intensive Applications. O'Reilly, 2017. Chapter 11 (Stream Processing).
Related patterns
Event Sourcing
Storing only the current state of an entity throws away the one thing a business often needs most - the history of how it got there. Audits become guesswork, "why did this happen?" becomes unanswerable, and rebuilding alternative projections of the data requires going back to the operational logs no one kept.
Transactional Outbox
Events published directly inside a database transaction can be lost if the broker is unavailable, leaving the database and downstream consumers permanently out of sync.