Last Updated: January 8, 2026
In traditional application architecture, we use the same model for reading and writing data. The same entity class, the same database table, the same service methods handle both updates and queries. This works well for many applications, but it creates tension when read and write requirements diverge significantly.
CQRS (Command Query Responsibility Segregation) resolves this tension by separating the read and write sides of an application. Commands handle updates to the system. Queries handle reads. Each side can be optimized independently.
The pattern builds on the event-driven concepts we explored in the previous chapter. Events often serve as the bridge between the write side (which publishes events) and the read side (which consumes events to build query-optimized views).
In this chapter, you will learn:
Imagine a typical e-commerce product catalog. You have one “Product Service” that does everything:
At first, a single service and a single database feels clean:
But as traffic grows, this simplicity turns into tension.
In most real systems, reads massively outnumber writes.
A typical e-commerce pattern looks like this:
Optimizing for one affects the other:
| Optimized For | Good At | Poor At |
|---|---|---|
| Writes | Fast updates, normalized data | Complex queries, aggregations |
| Reads | Fast queries, denormalized views | Updates require cascading changes |
So if you force both read and write traffic through the same model and schema, you end up making compromises in both directions.
Reads and writes often have fundamentally different needs:
| Aspect | Writes | Reads |
|---|---|---|
| Model | Domain entities with business rules | Flat DTOs optimized for display |
| Consistency | Must be strongly consistent | Can often be eventually consistent |
| Scaling | Scale based on transaction volume | Scale based on query volume |
| Caching | Generally not cached | Aggressively cached |
| Database | Relational for integrity | Document/search for flexibility |
CQRS separates the read model from the write model.
Commands express intent to change the system:=.
Queries request information without changing state.
CQRS can be applied at different levels of separation.
You separate command logic and query logic in code, but they still share the same database.
This is often the best first step because it delivers benefits without operational complexity.
You keep one database, but you stop pretending that one model fits all use cases.
You might still store both in the same database, but you treat them differently:
Benefits:
Now you split storage:
And you synchronize via events or replication.
This is the “classic” CQRS architecture, but it also comes with real operational cost.
The simplest form separates command and query handlers in code.
Benefits:
Limitations:
Use different models for reading and writing.
Benefits:
Full separation with different databases optimized for each purpose:
Write Database: PostgreSQL, MySQL for ACID transactions and referential integrity.
Read Database: Elasticsearch for search, Redis for caching, DynamoDB for key-value lookups.
Synchronization: Events flow from write side to update read models.
Once you separate the write database from the read database, you create a new problem:
The system is now correct in two places, and you have to keep both places aligned.
There are three common approaches: event-based (asynchronous), synchronous projection, and hybrid.
This is the most common CQRS sync approach.
This is the trade-off that powers CQRS at scale.
Sometimes you can’t tolerate lag. In those cases, you update the read model inside the same logical write operation.
Within one transaction boundary:
This approach works only when you can guarantee atomicity across both updates.
Synchronous projection is best reserved for critical read models where correctness matters more than throughput.
Most production systems mix both.
This gives you strong UX where it matters without forcing every read model into the hot write path.
Synchronization is about moving changes. Read model patterns are about what shape you move them into. The query side is not a copy of the write DB. It’s a set of purpose-built views.
Reads often want “pre-joined, display-ready” data.
Some read queries are expensive by nature: aggregates, group-bys, multi-dimensional stats.
Instead of running this repeatedly:
You store the result:
Then update it when relevant events occur (ProductCreated, ProductDeleted, PriceUpdated, etc.).
This is where CQRS becomes powerful: the same write-side truth can feed many read-side shapes.
Each read model optimized for specific query patterns.
With separate databases synchronized via events, consistency is eventual.
After a successful write, route the user’s next reads to the freshest source.
Common options:
This is a targeted solution: you don’t abandon the read model, you just avoid confusing the user right after a write.
If you can safely assume the write succeeded, update the UI immediately.
Flow:
This works well when:
After returning success, you can actively wait until the query side catches up.
Options:
This is common for workflows like:
Attach a monotonically increasing version to changes.
{ success: true, version: 42 }?minVersion=42version >= 42This gives you a precise definition of “caught up” without guessing based on time.
| Scenario | Why CQRS Helps |
|---|---|
| Read-heavy workloads | Optimize reads independently |
| Complex querying needs | Different databases for different queries |
| Different read/write teams | Teams can work independently |
| High-performance requirements | Cache and denormalize aggressively |
| Event-driven systems | Natural fit with event sourcing |
| Collaborative domains | Multiple views of same data |
| Scenario | Why CQRS Adds Overhead |
|---|---|
| Simple CRUD | Unnecessary complexity |
| Strong consistency required | Eventual consistency problematic |
| Small teams | Overhead of maintaining two models |
| Simple queries | No benefit from separation |
| Low traffic | Scaling benefits irrelevant |
Write Side: PostgreSQL handles all transactions with referential integrity.
Read Side:
CQRS separates the read and write sides of an application: