AlgoMaster Logo

CQRS (Command Query Responsibility Segregation)

Last Updated: January 8, 2026

Ashish

Ashish Pratap Singh

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:

  • The core principle of separating commands and queries
  • How CQRS differs from traditional CRUD architectures
  • Implementation approaches from simple to fully separated
  • The relationship between CQRS and event-driven architecture
  • Trade-offs and when CQRS is appropriate
  • Common patterns and best practices

The Problem CQRS Solves

Imagine a typical e-commerce product catalog. You have one “Product Service” that does everything:

  • Writes: add products, update price, update inventory
  • Reads: search, browse categories, filter/sort, product detail pages, recommendations widgets, etc.

At first, a single service and a single database feels clean:

  • One API
  • One model
  • One database schema
  • One set of code paths

But as traffic grows, this simplicity turns into tension.

Read-Write Asymmetry

In most real systems, reads massively outnumber writes.

A typical e-commerce pattern looks like this:

  • Reads: ~10,000 requests/second (search, browse, view product pages)
  • Writes: ~100 requests/second (price updates, inventory updates, product edits, purchases)
  • Ratio: 100:1 reads to writes

Optimizing for one affects the other:

Optimized ForGood AtPoor At
WritesFast updates, normalized dataComplex queries, aggregations
ReadsFast queries, denormalized viewsUpdates 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.

Different Requirements

Reads and writes often have fundamentally different needs:

AspectWritesReads
ModelDomain entities with business rulesFlat DTOs optimized for display
ConsistencyMust be strongly consistentCan often be eventually consistent
ScalingScale based on transaction volumeScale based on query volume
CachingGenerally not cachedAggressively cached
DatabaseRelational for integrityDocument/search for flexibility

The CQRS Principle

CQRS separates the read model from the write model.

  • Command side: handles writes (state changes)
  • Query side: handles reads (data retrieval)

Commands

Commands express intent to change the system:=.

Command Examples:

Characteristics:

  • Named as an imperative verb phrase
  • Contains all data needed to perform the action
  • Runs through validation and business rules
  • Returns success/failure (and maybe an ID)
  • Must be safe to retry (idempotency becomes important at scale)

Queries

Queries request information without changing state.

Query Examples:

Characteristics:

  • Return data transfer objects (DTOs)
  • Never modify state
  • Can be cached
  • Can use denormalized read models

Separation Levels

CQRS can be applied at different levels of separation.

Level 1: Code separation (same DB)

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.

Level 2: Model separation (different models, same DB)

You keep one database, but you stop pretending that one model fits all use cases.

  • Write side uses a domain model (invariants, business rules)
  • Read side uses read DTOs (denormalized, display-ready)

You might still store both in the same database, but you treat them differently:

  • write tables are normalized and integrity-focused
  • read tables/views/materialized views are optimized for queries

Benefits:

  • Write model stays clean and rule-driven
  • Read model can evolve quickly with UI/product needs
  • You can add precomputed fields without polluting the domain

Level 3: Database separation (full CQRS)

Now you split storage:

  • Write DB: optimized for transactions (Postgres/MySQL)
  • Read DB: optimized for queries/search (Elasticsearch, Redis, DynamoDB, ClickHouse, etc.)

And you synchronize via events or replication.

This is the “classic” CQRS architecture, but it also comes with real operational cost.

Implementing CQRS

Level 1: Code Separation

The simplest form separates command and query handlers in code.

Traditional service (mixed responsibilities)

CQRS-style separation

Benefits:

  • Clear separation of concerns
  • Each handler can be optimized for its purpose
  • Easy to understand and test

Limitations:

  • Still uses same database and model

Level 2: Model Separation

Use different models for reading and writing.

Write model (rich domain)

Read model (flat DTO)

Benefits:

  • Write model focused on business rules
  • Read model optimized for display
  • Different validation and transformation logic

Level 3: Database Separation

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.

Synchronization Strategies

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.

Event-Based Synchronization (Asynchronous)

This is the most common CQRS sync approach.

  • The command side writes to the write DB and then publishes an event describing what changed.
  • The query side consumes that event and updates one or more read models.

What you get

  • Fast writes: the write transaction stays focused.
  • Flexible reads: you can update multiple read stores (search, cache, analytics) from the same event stream.
  • Independent scaling: readers scale with read traffic; writers scale with write traffic.

The cost: eventual consistency

  • The read model lags behind the write model.
  • Lag is typically milliseconds to seconds, but can be longer during spikes, deploys, or incidents.
  • Your UI and APIs must be designed with the assumption that “read after write” may temporarily show older data.

This is the trade-off that powers CQRS at scale.

Synchronous Projection (Strong Consistency)

Sometimes you can’t tolerate lag. In those cases, you update the read model inside the same logical write operation.

Within one transaction boundary:

  1. Update write database
  2. Update read database
  3. Commit

Why it’s tricky

This approach works only when you can guarantee atomicity across both updates.

  • If both “databases” are actually the same database (just different tables/views), it can be straightforward.
  • If they are different systems (e.g., Postgres + Elasticsearch), doing a true atomic commit requires distributed transactions, which most teams avoid due to complexity and performance cost.

Pros

  • Immediate consistency: read model is correct as soon as the command returns.

Cons

  • Slower writes (you’re doing more work per transaction).
  • Tight coupling (write path now depends on read store availability).
  • Harder operations (a read-store outage can block writes).

Synchronous projection is best reserved for critical read models where correctness matters more than throughput.

Hybrid Approaches

Most production systems mix both.

  • Keep the write path fast and reliable.
  • Update critical “must-be-correct-now” reads synchronously.
  • Update everything else asynchronously.

A practical hybrid setup

  • Cache layer updated synchronously on write (for “read-your-writes” experiences)
  • Search index updated asynchronously via events
  • Analytics / aggregates updated asynchronously via events

This gives you strong UX where it matters without forcing every read model into the hot write path.

Read Model Patterns

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.

Denormalized Views

Reads often want “pre-joined, display-ready” data.

Materialized Views

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.).

Multiple Read Models

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.

Handling Eventual Consistency

With separate databases synchronized via events, consistency is eventual.

The User Experience Challenge

Strategies

1. Read-Your-Writes

After a successful write, route the user’s next reads to the freshest source.

Common options:

  • Read from the write DB for a short window.
  • Read from a write-through cache updated synchronously.
  • Use a “session consistency” rule: for user X, prefer primary source for N seconds.

This is a targeted solution: you don’t abandon the read model, you just avoid confusing the user right after a write.

2. Optimistic UI

If you can safely assume the write succeeded, update the UI immediately.

Flow:

  • User clicks “Save”
  • UI immediately reflects the new state
  • Backend sync happens in the background
  • If something fails, you show a correction message (rare, but handled)

This works well when:

  • writes are usually successful
  • the update is user-local (profile info, settings, preferences)
  • the consequences of temporary mismatch are low

3. Polling or Push

After returning success, you can actively wait until the query side catches up.

Options:

  • Poll the query API until it reflects the change
  • Use push notifications / websockets / SSE to notify when projection is updated

This is common for workflows like:

  • uploading an image
  • generating a report
  • processing an order state change

4. Version Tracking

Attach a monotonically increasing version to changes.

  • Command returns: { success: true, version: 42 }
  • Query supports: ?minVersion=42
  • Query handler waits (briefly) until the read model has version >= 42

This gives you a precise definition of “caught up” without guessing based on time.

When to Use CQRS

Good Fit

ScenarioWhy CQRS Helps
Read-heavy workloadsOptimize reads independently
Complex querying needsDifferent databases for different queries
Different read/write teamsTeams can work independently
High-performance requirementsCache and denormalize aggressively
Event-driven systemsNatural fit with event sourcing
Collaborative domainsMultiple views of same data

Poor Fit

ScenarioWhy CQRS Adds Overhead
Simple CRUDUnnecessary complexity
Strong consistency requiredEventual consistency problematic
Small teamsOverhead of maintaining two models
Simple queriesNo benefit from separation
Low trafficScaling benefits irrelevant

Decision Heuristic

Consider CQRS when:

  1. Read and write patterns are very different
  2. Read-to-write ratio is high (10:1 or more)
  3. Complex query requirements (search, aggregations)
  4. Need to scale reads and writes independently
  5. Already using event-driven architecture

Avoid CQRS when:

  1. Simple CRUD operations
  2. Low traffic
  3. Strong consistency is essential
  4. Small team with limited bandwidth

CQRS in Practice

E-Commerce Example

Write Side: PostgreSQL handles all transactions with referential integrity.

Read Side:

  • Elasticsearch for product search
  • Redis for shopping cart (fast access)
  • ClickHouse for analytics dashboards

Summary

CQRS separates the read and write sides of an application:

  • Core principle: Commands change state, queries read state, each optimized independently
  • Levels of separation: Code only, models only, or full database separation
  • Synchronization: Events flow from write side to update read models
  • Eventual consistency: Read models may lag behind writes
  • Advantages: Independent optimization, scaling, simpler focused models, technology flexibility
  • Disadvantages: Increased complexity, eventual consistency, synchronization challenges
  • Best for: Read-heavy systems, complex queries, high-scale requirements