September 2, 2026 79 minutes minutes read Admin

CQRS Pattern - Separating Reads and Writes in Distributed Systems

The Problem: One Model Trying to Do Everything

Imagine an Order Service.

It needs to support operations such as:

Create Order
Update Order
Cancel Order
Get Order
Search Orders
Get Order History
Get Customer Orders
Get Order Dashboard
Get Sales Report

At first, this is simple.

You might have:

                  Order Service
                       |
                       v
                  Order Model
                       |
                       v
                   Database

The same model is used for:

Writes
  |
  +-- Create
  +-- Update
  +-- Delete

Reads
  |
  +-- Get by ID
  +-- Search
  +-- Reports
  +-- Dashboards

This is perfectly reasonable.

For many applications, it is exactly what you should build.

But as the system becomes more complicated, something interesting happens.

The requirements for reading data and writing data start becoming very different.

The write side may need:

Business rules
Validation
Transactions
Concurrency control
Domain logic
Aggregates
Authorization
Consistency

while the read side may need:

Fast queries
Denormalized data
Joins
Filtering
Sorting
Pagination
Aggregations
Search
Caching
Read replicas

Now one model is trying to satisfy two very different responsibilities.

This is where CQRS becomes useful.


What Is CQRS?

CQRS stands for:

Command Query Responsibility Segregation

The fundamental idea is:

Separate operations that change state from operations that read state.

Instead of treating the application as one CRUD model:

             Application
                  |
             Order Model
             /         \
          Write        Read
            \           /
             \         /
               Database

we separate the responsibilities:

                 Application
                      |
          +-----------+-----------+
          |                       |
          v                       v
     Command Side            Query Side
          |                       |
          v                       v
     Write Model              Read Model
          |                       |
          v                       v
       Database              Read Store

The important thing is not necessarily that there are two databases.

The important thing is:

Command responsibility
        ≠
Query responsibility

Martin Fowler describes CQRS as using a different model for updating information than the model used for reading it. He also emphasizes that CQRS is useful in particular situations but adds complexity and is not appropriate for most ordinary systems.

That distinction is extremely important.


CQRS Is Not "Two Databases"

This is probably the most common misunderstanding about CQRS.

People often hear:

CQRS

and immediately think:

Write Database
       |
       v
Kafka
       |
       v
Read Database

That is one possible architecture.

It is not CQRS itself.

CQRS can exist with:

One application
One database
Two models

For example:

                Application
                     |
          +----------+----------+
          |                     |
          v                     v
     Command Model         Query Model
          |                     |
          +----------+----------+
                     |
                  PostgreSQL

The command and query models can use the same underlying database.

Microsoft's architecture guidance explicitly describes this as a basic form of CQRS: separate write and read models while using a shared data store.

You can then evolve toward:

Command Model
     |
     v
Write Database

Query Model
     |
     v
Read Database

if the system actually benefits from that separation.


Start With CRUD

To understand why CQRS exists, let's first understand the traditional CRUD approach.

Suppose we have:

orders

with:

id
customer_id
status
total_amount
shipping_address
billing_address
created_at
updated_at

A traditional application might have:

class Order {
    Long id;
    Long customerId;
    Status status;
    BigDecimal totalAmount;
    Address shippingAddress;
    Address billingAddress;
    Instant createdAt;
    Instant updatedAt;
}

The same object might be used for:

Create
Read
Update
Delete

This is simple.

For example:

GET /orders/123

might return:

{
  "id": 123,
  "customerId": 456,
  "status": "SHIPPED",
  "totalAmount": 120.00,
  "shippingAddress": {
    "city": "Kathmandu"
  }
}

And:

PUT /orders/123

might receive:

{
  "status": "CANCELLED"
}

The same underlying representation is involved in both operations.

For a simple application, this is excellent.

The problem begins when the application becomes much more complicated.


Reads and Writes Have Different Goals

Consider what happens when we create an order.

The system might need to enforce:

Order must contain at least one item
Customer must be active
Inventory must be available
Payment must be authorized
Order cannot be cancelled after shipment
Total must be calculated correctly

The write side therefore needs substantial business logic.

But consider the order dashboard.

It might need:

Order ID
Customer Name
Product Names
Payment Status
Shipment Status
Warehouse
Total
Discount
Last Updated

The dashboard may need information from:

orders
customers
payments
shipments
products
warehouses

A query could look like:

SELECT
    o.id,
    c.name,
    p.status,
    s.status,
    SUM(oi.quantity * oi.price) AS total
FROM orders o
JOIN customers c ON ...
JOIN payments p ON ...
JOIN shipments s ON ...
JOIN order_items oi ON ...
GROUP BY ...

Notice what happened.

The write model is about:

Maintaining correct business state

The read model is about:

Efficiently answering a particular question

Those are different problems.


The Fundamental Separation

CQRS gives us two concepts.

Commands

A command represents an intention to change state.

Examples:

CreateOrder
CancelOrder
PayOrder
ShipOrder
AddItemToOrder
RemoveItemFromOrder
ChangeShippingAddress

A command answers:

What does the user want the system to do?

Commands should generally represent business actions rather than low-level field mutations.

For example:

BookHotelRoom

is more meaningful than:

SetReservationStatus = RESERVED

Microsoft's CQRS guidance explicitly recommends task-oriented commands because they better express business intent.


Queries

A query asks for information.

Examples:

GetOrder
SearchOrders
GetCustomerOrders
GetOrderDashboard
GetOrderHistory
GetSalesSummary

A query answers:

What information does the caller need?

A query should not change application state.

This is closely related to the older Command Query Separation (CQS) principle: commands change state, while queries return information without changing observable state.

CQRS takes that idea beyond individual methods and applies it to application architecture.


CQRS at the API Level

A traditional CRUD API might look like:

POST   /orders
GET    /orders/{id}
PUT    /orders/{id}
DELETE /orders/{id}

The CQRS distinction is more conceptual:

Command API
    |
    +-- CreateOrder
    +-- CancelOrder
    +-- ShipOrder
    +-- ChangeAddress

Query API
    |
    +-- GetOrder
    +-- SearchOrders
    +-- GetOrderHistory
    +-- GetOrderDashboard

You do not necessarily need separate HTTP services.

You can implement both inside the same application.

The important distinction is:

Commands
    ↓
Business behavior

Queries
    ↓
Data retrieval

A Simple CQRS Architecture

The simplest useful implementation could be:

                    Client
                      |
                +-----+-----+
                |           |
                v           v
            Commands      Queries
                |           |
                v           v
        Command Handlers Query Handlers
                |           |
                v           v
          Write Model    Read Model
                |           |
                +-----+-----+
                      |
                  PostgreSQL

There are still:

One application
One database

but there are two separate models.

This is often the best place to start.


Why Separate the Models?

Because the models have different jobs.

The write model might look like:

Order
 |
 +-- Customer
 +-- OrderItem
 +-- Payment
 +-- Shipment

It may be highly normalized and designed around domain invariants.

The read model might look like:

OrderSummary

+---------+----------+---------+---------+
| orderId | customer | status  | total   |
+---------+----------+---------+---------+

It is designed around what users need to see.

The two models don't have to look alike.

That is one of the most important ideas in CQRS.


The Write Model

The write model is responsible for changing state correctly.

It typically contains:

Commands
Command Handlers
Domain Model
Validation
Business Rules
Transactions
Repositories
Concurrency Control

For example:

CreateOrderCommand
        |
        v
CreateOrderHandler
        |
        v
Order Aggregate
        |
        v
OrderRepository
        |
        v
Database

The write side asks:

Is this operation allowed?

not simply:

Can I update this row?

The Query Model

The query model has a completely different goal.

It asks:

What is the fastest and simplest way to answer this query?

For example:

GetOrderDashboard

might return:

{
  "orderId": 123,
  "customer": "Alice",
  "status": "SHIPPED",
  "payment": "PAID",
  "shipment": "IN_TRANSIT",
  "total": 120.00
}

The query model does not need to reconstruct the entire domain object.

It can simply return a DTO designed for that screen.

Microsoft's guidance describes queries as returning DTOs optimized for presentation and without domain logic.


The Read Model Can Be Denormalized

Suppose the write database has:

customers
orders
order_items
products
payments
shipments

The read model could instead have:

order_dashboard

containing:

order_id
customer_name
order_status
payment_status
shipment_status
total_amount
item_count

Now the dashboard query is trivial:

SELECT *
FROM order_dashboard
WHERE order_id = 123;

Instead of repeatedly performing:

JOIN
JOIN
JOIN
JOIN
GROUP BY

for every request.

This is one of the major benefits of CQRS.


Materialized Views

A read model is often implemented as a materialized view or a projection.

Conceptually:

Write Model
     |
     | Events / changes
     v
Projection
     |
     v
Read Model

For example:

OrderCreated
OrderPaid
OrderShipped

can update:

order_dashboard

so that the read side always has a precomputed representation.

Instead of calculating:

Customer + Order + Payment + Shipment

for every request, we calculate it when the underlying data changes.

Then:

GET /orders/123

becomes very cheap.


CQRS With One Database

Let's start with the least complicated architecture.

                Application
                     |
          +----------+----------+
          |                     |
          v                     v
   Command Handler        Query Handler
          |                     |
          v                     v
     Write Model            Read Model
          |                     |
          +----------+----------+
                     |
                  Database

The write side might use:

JPA/Hibernate

while the query side might use:

JdbcTemplate
jOOQ
native SQL

There is no reason the read side has to use the same ORM model.

For example:

public interface OrderQueryRepository {

    OrderSummary findById(Long orderId);

    List<OrderSummary> search(
        String customer,
        String status
    );
}

The implementation could use a highly optimized SQL query.

Meanwhile:

public interface OrderRepository {

    Order save(Order order);

    Optional<Order> findById(Long id);
}

is responsible for the domain model.

That is already CQRS.


Why Use Different Data Access Technologies?

The write side may benefit from:

ORM
Entity Mapping
Aggregate Mapping
Transactions
Optimistic Locking

The read side may benefit from:

SQL
Projections
Database Views
Indexes
Denormalized Tables
Search Engines
Caching

Trying to force both sides through the same abstraction can create unnecessary complexity.

CQRS gives each side freedom to use the right tool.


Separate Databases

Now suppose the application grows.

Reads are:

100,000 requests/sec

Writes are:

1,000 requests/sec

The requirements are obviously different.

We can scale the read side independently:

                  Client
                    |
          +---------+---------+
          |                   |
          v                   v
      Commands             Queries
          |                   |
          v                   v
     Write Service       Query Service
          |                   |
          v                   v
    Write Database       Read Database
                              |
                       +------+------+
                       |      |      |
                      DB     Cache  Search

Now CQRS becomes a scaling architecture.

AWS describes this as a useful scenario when command and query workloads have different throughput, latency, or consistency requirements.


Read Replicas vs CQRS

This distinction matters.

Suppose we have:

Primary Database
      |
      +---- Read Replica 1
      +---- Read Replica 2
      +---- Read Replica 3

This improves read scalability.

But it does not necessarily mean we have CQRS.

Why?

Because we might still have:

Same Model
Same Queries
Same Representation

with the only difference being:

Database replication

CQRS is about:

Responsibility
+
Model separation

Read replicas are primarily about:

Scaling reads

They can be used together, but they are not the same thing.


CQRS With Different Storage Technologies

Once reads and writes are separated, the read side does not necessarily need to use the same database technology.

For example:

Write Side
    |
    v
PostgreSQL

while:

Read Side
    |
    v
Elasticsearch

or:

Write Side
    |
    v
PostgreSQL

Read Side
    |
    v
Redis

or:

Write Side
    |
    v
PostgreSQL

Read Side
    |
    v
MongoDB

The choice should be driven by query requirements.

For example:

Relational database
    |
    +-- Strong transactions
    +-- Complex writes
    +-- Referential integrity

Search engine
    |
    +-- Full-text search
    +-- Flexible filtering
    +-- Search ranking

CQRS allows the read side to use the storage technology that best fits its workload.


How Does the Read Model Get Updated?

This is where things become more interesting.

Suppose:

Order Service

updates:

orders

but the read model lives in:

order_read_model

Something needs to synchronize them.

A common architecture is:

Command
   |
   v
Write Model
   |
   v
Write Database
   |
   v
Event
   |
   v
Read Model Projector
   |
   v
Read Database

For example:

CreateOrder
     |
     v
Order Service
     |
     +---- PostgreSQL
     |
     +---- OrderCreated
                |
                v
        Read Model Projector
                |
                v
        order_dashboard

Now:

Write database

and:

Read database

are separate.


This Introduces Eventual Consistency

Suppose the user creates an order.

At:

10:00:00.000

the write succeeds.

But the read projection receives the event at:

10:00:00.150

For 150 milliseconds:

Write Model:
Order exists ✓

Read Model:
Order does not exist yet

This is eventual consistency.

The read model eventually catches up.

Microsoft explicitly identifies eventual consistency as one of the main trade-offs of separating read and write data stores.


Eventual Consistency Is Not Automatically a Bug

It depends on the requirement.

For:

Analytics dashboard

a delay of:

1 second

may be completely acceptable.

For:

Bank account balance

it may not be acceptable to show stale data after a transaction.

Therefore, CQRS should be driven by business requirements.

Ask:

How stale can the read model be?

before introducing asynchronous projections.


The Read-After-Write Problem

A common problem appears immediately.

Suppose:

POST /orders

creates:

Order 123

The client immediately calls:

GET /orders/123

But the query model has not processed the event yet.

The response might be:

404 Not Found

even though the order was successfully created.

This is a classic CQRS problem.

Possible solutions include:

Return the write-side result

or:

Read from the write model immediately after a write

or:

Use consistency tokens

or:

Wait until the projection reaches a required version

or:

Use synchronous projection for critical paths

There is no universal solution.

The right choice depends on the consistency requirements of the application.


Strong Consistency Where It Matters

CQRS does not mean:

Everything must be eventually consistent.

You can have:

Command
   |
   v
Write Model
   |
   +---- Critical read → Write Model
   |
   +---- Dashboard → Read Model

For example:

Account balance

may be read from the authoritative write model.

While:

Monthly spending dashboard

may use an eventually consistent projection.

This is often a much more practical architecture than forcing every read through an eventually consistent read store.


CQRS Does Not Require Messaging

Another common misconception is:

CQRS = Kafka

No.

You can have:

Command Model
      |
      v
Database
      |
      v
Query Model

without a message broker.

You can even have:

One database
One application
Two models

and still have CQRS.

Messaging becomes useful when:

Read model is separate

and needs asynchronous updates.

But messaging is an implementation choice, not the definition of CQRS.


CQRS Does Not Require Event Sourcing

This distinction is extremely important.

You can have:

CQRS
+
Traditional relational database

without Event Sourcing.

For example:

Command
   |
   v
orders table

and:

Query
   |
   v
order_dashboard table

That is CQRS.

Event Sourcing is a different pattern.

With Event Sourcing:

Events
   |
   +-- OrderCreated
   +-- OrderPaid
   +-- OrderShipped
   |
   v
Current State

The events are the source of truth.

With ordinary CQRS:

Current State
   |
   +-- Write Model
   |
   +-- Read Model

The write database can remain the source of truth.

Microsoft also treats CQRS and Event Sourcing as separate patterns that can be combined but do not have to be.


CQRS + Event Sourcing

When the two patterns are combined:

                    Command
                       |
                       v
                 Command Handler
                       |
                       v
                  Event Store
                       |
             +---------+---------+
             |                   |
             v                   v
        Event Stream       Projection Engine
                                 |
                                 v
                           Read Database

For example:

OrderCreated
OrderPaid
OrderShipped

are stored permanently.

The read model is built from them:

Events
   |
   v
Projection
   |
   v
Order Dashboard

One major advantage is that if you need a new read model, you can rebuild it from the event history.

But this also introduces significantly more complexity.


CQRS + Transactional Outbox

This is where the previous article in this series becomes directly relevant.

Suppose the write model performs:

Create Order

and needs to publish:

OrderCreated

We have the same dual-write problem:

Write Database
       +
Message Broker

The Transactional Outbox solves it.

The write side does:

BEGIN

Create Order
Insert OrderCreated into Outbox

COMMIT

Then:

Outbox
   |
   v
Publisher
   |
   v
Message Broker
   |
   v
Read Model Projector
   |
   v
Read Database

So now the patterns fit together naturally:

CQRS
 |
 +-- Separate command and query responsibilities
 |
 +-- Outbox reliably publishes changes
 |
 +-- Broker distributes events
 |
 +-- Projection builds read models

This is a very common architecture for event-driven systems.


CQRS and the Outbox Are Different

It is useful to keep their responsibilities separate.

CQRS
 |
 +-- Separates reads from writes

while:

Transactional Outbox
 |
 +-- Makes database change + outgoing event atomic

And:

Event Broker
 |
 +-- Transports events

And:

Projection
 |
 +-- Builds read model

So:

CQRS ≠ Outbox
CQRS ≠ Kafka
CQRS ≠ Event Sourcing
CQRS ≠ Microservices

They can work together.

But they solve different problems.


A Complete CQRS Architecture

Let's put everything together.

                         Client
                           |
             +-------------+-------------+
             |                           |
             v                           v
         Commands                     Queries
             |                           |
             v                           v
      Command Handler              Query Handler
             |                           |
             v                           v
        Write Model                 Read Model
             |                           |
             v                           v
      Write Database              Read Database
             |
             v
          Outbox
             |
             v
       Message Broker
             |
             v
       Read Projector
             |
             v
        Read Database

The responsibilities are now clearly separated.

Command Side
    |
    +-- Business rules
    +-- Validation
    +-- Transactions
    +-- Consistency
    +-- State changes

Query Side
    |
    +-- Fast retrieval
    +-- DTOs
    +-- Filtering
    +-- Sorting
    +-- Search
    +-- Read optimization

A Real Example: Order Management

Suppose we are building an e-commerce platform.

The business has:

10 million orders

Users frequently perform:

Search orders
View order details
Filter by status
View dashboard
View customer history

But writes are relatively infrequent.

For example:

Reads:
1,000,000 / minute

Writes:
20,000 / minute

The traditional model might struggle because the same database has to support:

Complex writes
+
Massive read traffic

CQRS allows us to separate them.


Command Side

The command:

CreateOrder

goes to:

Command Handler

The handler:

Validate request
      |
      v
Check business rules
      |
      v
Create Order
      |
      v
Persist Order
      |
      v
Write Outbox

The transaction is:

BEGIN

INSERT order
INSERT outbox event

COMMIT

Query Side

The event:

OrderCreated

is published.

The projector receives it:

OrderCreated
      |
      v
Read Model Projector
      |
      v
INSERT INTO order_dashboard

Now the query:

GET /orders/123

can simply execute:

SELECT *
FROM order_dashboard
WHERE order_id = 123;

The read model is optimized specifically for that query.


Different Read Models for Different Screens

This is one of the most powerful aspects of CQRS.

You don't need one universal read model.

You can have:

Order Details Projection

for:

GET /orders/{id}

and:

Customer Orders Projection

for:

GET /customers/{id}/orders

and:

Order Dashboard Projection

for:

GET /dashboard/orders

and:

Sales Report Projection

for:

GET /reports/sales

Conceptually:

                     Events
                       |
          +------------+------------+
          |            |            |
          v            v            v
     Order View   Customer View   Sales View

Each projection is optimized for a particular query workload.

This is sometimes called polyglot read modeling or simply maintaining multiple read projections.


Why This Can Be Extremely Fast

Suppose a dashboard requires:

20 joins

and:

5 aggregations

If every request executes those operations:

Request
   |
   v
20 joins
   |
   v
5 aggregations
   |
   v
Response

you are repeating expensive computation.

With a projection:

Write Event
     |
     v
Compute once
     |
     v
Store result

Then:

Request
   |
   v
SELECT precomputed row
   |
   v
Response

The expensive work happens when data changes rather than every time data is read.


CQRS as Precomputation

This gives us another useful mental model.

Traditional querying:

Every request
     |
     v
Compute answer

CQRS projection:

Data changes
     |
     v
Compute answer
     |
     v
Store answer
     |
     v
Every request reads answer

So CQRS can effectively turn expensive query computation into an asynchronous preprocessing step.


Scaling the Read Side

Suppose:

Command traffic:
1,000 requests/sec

Query traffic:
100,000 requests/sec

With CQRS:

                    Queries
                       |
             +---------+---------+
             |         |         |
             v         v         v
          Query-1   Query-2   Query-3
             |         |         |
             +---------+---------+
                       |
                  Read Database

You can scale the query side independently.

Meanwhile:

Commands
   |
   v
Command Service
   |
   v
Write Database

This avoids scaling the entire application based on the dominant read workload.

Microsoft identifies independent scaling as one of CQRS's major benefits.


Scaling the Write Side

The write side has different constraints.

Writes often involve:

Transactions
Locks
Concurrency
Business invariants
Aggregate consistency

You may not want to blindly add hundreds of write instances.

Instead, you might keep:

Smaller command-processing cluster

while having:

Large query cluster

because the workloads are fundamentally different.


CQRS and Concurrency

CQRS can also help with concurrency problems.

Suppose two users try:

Buy last available ticket

at the same time.

The command side can enforce:

Only one command succeeds.

The write model can use:

Optimistic locking
Pessimistic locking
Database constraints
Aggregate versioning

The read model does not need to enforce these business invariants.

Its job is:

Show the current projection.

This keeps the complex consistency logic concentrated on the write side.


Optimistic Concurrency

For example:

Order version = 10

Client sends:

CancelOrder
expectedVersion = 10

The command handler executes:

UPDATE orders
SET status = 'CANCELLED',
    version = 11
WHERE id = 123
AND version = 10;

If zero rows are updated:

Someone else changed the order.

The command fails.

This is a write-side concern.

The query side simply reads:

Order status

It does not need to understand the concurrency protocol.


Commands Should Express Intent

Consider:

UpdateOrderStatus

versus:

ShipOrder

The second is often a better command.

Why?

Because:

ShipOrder

can express business rules:

Payment must be completed
Inventory must be reserved
Order must not already be cancelled
Shipment address must exist

while:

UpdateOrderStatus

can allow callers to bypass those rules.

A command should therefore represent:

Business intent

rather than:

Database mutation

This is one of the most important modeling ideas in CQRS.


Commands Are Not CRUD Requests

A weak CQRS implementation might simply rename CRUD:

CreateOrderCommand
UpdateOrderCommand
DeleteOrderCommand

and call it CQRS.

That misses much of the value.

A richer domain might use:

PlaceOrder
AddItemToOrder
RemoveItemFromOrder
ApplyDiscount
ConfirmPayment
ShipOrder
CancelOrder
RefundOrder

Each command represents an action.

The command handler becomes the place where the business operation is implemented.


Queries Should Be Boring

This is a useful CQRS principle.

The command side should contain:

Complexity

The query side should ideally contain:

Simple retrieval

For example:

public OrderDetails getOrder(long orderId) {
    return jdbcTemplate.queryForObject(
        """
        SELECT ...
        FROM order_details_view
        WHERE order_id = ?
        """,
        mapper,
        orderId
    );
}

There should be little or no:

Business decision
Mutation
Transaction
Domain behavior

in the query handler.

The query side should answer:

"Here is the data you asked for."


Security Benefits

CQRS can also help with security.

The command side can expose only:

Allowed business operations

For example:

CancelOrder
RefundOrder
ChangeAddress

instead of allowing clients to submit arbitrary:

UPDATE orders
SET ...

The query side can expose only the fields appropriate for a particular user.

For example:

Customer View

might contain:

Order ID
Status
Total
Shipment

while:

Internal Finance View

might contain:

Payment Provider
Transaction ID
Fees
Tax
Settlement Status

Separate read models can therefore support different security boundaries.

Microsoft identifies separation of read and write responsibilities as a potential security benefit of CQRS.


CQRS and Caching

CQRS works particularly well with caching.

For example:

Read Model
    |
    v
Redis

The query side can serve:

GET /product/123

directly from a cache.

The write side remains authoritative:

PostgreSQL

When the product changes:

ProductUpdated
      |
      v
Invalidate / update cache

The key is that the cache is part of the read strategy, not the source of truth.


CQRS and Search

Search is another strong use case.

Suppose your transactional database is:

PostgreSQL

but users need:

Full-text search
Fuzzy matching
Filtering
Ranking
Facets

A query model could be:

PostgreSQL
     |
     v
ProductUpdated
     |
     v
Search Projection
     |
     v
Elasticsearch

Now:

Commands
    |
    v
PostgreSQL

while:

Queries
    |
    v
Elasticsearch

The write model remains focused on transactional correctness.

The query model is optimized for search.


CQRS and Reporting

Reporting is another common reason to separate reads.

Transactional schemas are often optimized for:

OLTP

while reports need:

Aggregations
Joins
Historical analysis
Grouping
Large scans

Instead of making the transactional database perform huge analytical queries:

Production DB
      |
      +-- Transactions
      +-- User requests
      +-- Reports

you can create a dedicated projection:

Production DB
      |
      v
Events / ETL
      |
      v
Reporting Store
      |
      v
Reports

Again, the read model is optimized for its workload.


CQRS and Microservices

CQRS does not require microservices.

You can implement CQRS inside a monolith:

Monolith
 |
 +-- Command Module
 |
 +-- Query Module
 |
 +-- Database

This can actually be a very good starting point.

Later, if the system needs independent deployment or scaling:

Command Module
      |
      v
Command Service

Query Module
      |
      v
Query Service

The architecture can evolve without changing the fundamental responsibility separation.


CQRS Inside a Monolith

A practical Java project might look like:

src/main/java/com/example/order

    command/
        CreateOrderCommand.java
        CreateOrderHandler.java
        CancelOrderCommand.java
        CancelOrderHandler.java

    query/
        GetOrderQuery.java
        GetOrderHandler.java
        SearchOrdersQuery.java
        SearchOrdersHandler.java

    domain/
        Order.java
        OrderItem.java
        OrderRepository.java

    infrastructure/
        OutboxRepository.java

This gives you CQRS without introducing:

Kafka
Multiple databases
Microservices
Distributed transactions

That is often the right way to learn and adopt CQRS.


A Practical Spring Boot Structure

For example:

                    REST API
                       |
          +------------+------------+
          |                         |
          v                         v
    Command Controller        Query Controller
          |                         |
          v                         v
    Command Handler            Query Handler
          |                         |
          v                         v
    Domain Model             Read Repository
          |                         |
          v                         v
    JPA Repository            JDBC / jOOQ
          |                         |
          +------------+------------+
                       |
                   PostgreSQL

The command side can use:

Spring Data JPA

while the query side can use:

JdbcTemplate

or:

jOOQ

There is no requirement that both use the same persistence model.


The Query DTO Is Not the Domain Entity

This is another important distinction.

Avoid:

OrderEntity

being returned directly from every query.

Instead:

public record OrderSummary(
    Long orderId,
    String customerName,
    String status,
    BigDecimal total
) {}

The query model can return exactly what the client needs.

For example:

OrderSummary
OrderDetails
OrderDashboard
CustomerOrderHistory
SalesSummary

Each can have a different shape.

That is one of the biggest practical benefits of separating read and write models.


Multiple Read Models

Suppose we have:

OrderCreated
OrderPaid
OrderShipped
OrderCancelled

We can build:

                Events
                   |
       +-----------+-----------+
       |           |           |
       v           v           v
 Order Details  Dashboard    Reporting
 Projection     Projection    Projection
       |           |           |
       v           v           v
    Database     Redis       Analytics DB

Each read model has a different purpose.

This is difficult to achieve cleanly when every query is forced through the same domain model.


Rebuilding a Read Model

One of the great benefits of event-driven CQRS is the ability to rebuild projections.

Suppose:

Order Dashboard Projection

has a bug.

If the authoritative history is available:

Events

you can rebuild:

DROP old projection

Replay events

OrderCreated
OrderPaid
OrderShipped
...

and produce:

New Projection

This becomes particularly powerful when CQRS is combined with Event Sourcing.

But if your CQRS implementation uses only current-state tables and no durable event history, rebuilding a projection may require:

Database queries
ETL
Backfills
Reprocessing

So projection rebuildability depends on the underlying architecture.


Projection Failures

Suppose:

OrderCreated

is successfully published.

But:

Read Model Projector

is down.

Then:

Write Database:
Order exists ✓

Read Database:
Order missing

This is not necessarily data loss.

The event can remain in:

Broker

and the projector can catch up later.

This is one of the benefits of asynchronous read models.

A temporary read-side failure does not necessarily stop the write side.


Projection Lag

Just as the Outbox has:

Outbox Lag

a CQRS system has:

Projection Lag

For example:

Event timestamp:
10:00:00

Projection processed:
10:00:02

Projection lag:
2 seconds

This should be monitored.

Useful metrics include:

Projection lag
Events processed/sec
Projection failures
Retry count
Consumer lag
Dead-letter messages
Read model freshness

A CQRS architecture without observability can be extremely difficult to debug.


Handling Duplicate Events

If the read model is updated from a message broker, duplicate events are possible.

For example:

OrderCreated
OrderCreated

The projection should be idempotent.

One approach is to track:

event_id

and reject events that have already been processed.

For example:

processed_events

+----------------+
| event_id       |
+----------------+
| abc-123        |
+----------------+

The projector can perform:

BEGIN

Check event ID

If already processed:
    ignore

Otherwise:
    update projection
    record event ID

COMMIT

This is another place where the Transactional Outbox + Idempotency article becomes directly relevant.


CQRS and Eventual Consistency Require UX Design

Eventually consistent systems can produce confusing user interfaces.

Imagine:

User:
Create Order

Response:
201 Created
Order ID = 123

Then immediately:

GET /orders/123

returns:

404

The system may technically be correct.

But the user experience is terrible.

Therefore CQRS is not only a backend architecture problem.

The UI/API contract must understand:

What becomes available immediately?
What becomes available eventually?
How long can propagation take?

Possible approaches include:

Return command result

or:

Return resource state directly from command side

or:

Expose projection status

For example:

{
  "orderId": "123",
  "status": "PROCESSING",
  "readModelReady": false
}

The architecture must make consistency behavior explicit.


CQRS Does Not Automatically Improve Performance

This is another important misconception.

Simply doing:

Command
+
Query

does not make a system faster.

You gain performance when the separation allows you to do something useful:

Denormalize reads
Scale reads independently
Use specialized databases
Precompute results
Use search engines
Reduce contention
Optimize queries separately

If you have:

Simple CRUD application

and introduce:

Commands
Queries
Handlers
Events
Broker
Read database
Projection
Retries
Monitoring

you may make the system significantly slower to develop and operate.

CQRS is valuable when the separation buys you something meaningful.


CQRS Adds Complexity

A traditional application:

API
 |
 v
Service
 |
 v
Database

may become:

API
 |
 +---- Command Service
 |          |
 |          v
 |      Write DB
 |
 +---- Query Service
            |
            v
         Read DB

Write DB
   |
   v
Outbox
   |
   v
Broker
   |
   v
Projector
   |
   v
Read DB

Now you have:

More components
More deployments
More failure modes
More monitoring
More operational work
Eventual consistency
Duplicate handling
Replay concerns
Schema evolution

That complexity must be justified.

Martin Fowler's warning is important here: CQRS can be valuable for a minority of systems, while applying it to systems that do not need it can increase complexity and risk.


When CQRS Is a Good Fit

CQRS is particularly useful when you have:

1. Very Different Read and Write Workloads

For example:

Reads:
1,000,000/sec

Writes:
10,000/sec

Independent scaling can be valuable.


2. Complex Business Rules

For example:

Banking
Insurance
Trading
Order management
Booking systems

where commands represent meaningful domain operations.

The write model can focus heavily on:

Correctness
Consistency
Business invariants

while the query model focuses on:

Presentation
Search
Reporting

3. Complex Queries

If your application constantly needs:

20-table joins
Large aggregations
Search
Multiple projections

a dedicated read model can simplify and accelerate queries.


4. Different Storage Requirements

For example:

Write:
PostgreSQL

Read:
Elasticsearch

or:

Write:
PostgreSQL

Read:
Redis

CQRS makes the architectural separation explicit.


5. Task-Based Applications

Applications where users perform meaningful business actions:

Book Flight
Approve Loan
Cancel Subscription
Reserve Inventory
Process Refund

often benefit from command-oriented modeling.

Microsoft identifies task-based user interfaces and complex domain models as scenarios where CQRS can be beneficial.


When CQRS Is a Bad Fit

Avoid CQRS when your application is mostly:

Create
Read
Update
Delete

with:

Simple business rules
Simple queries
Moderate traffic
One database
No unusual scaling requirements

For example:

Internal admin CRUD application

may not benefit from:

Command handlers
Query handlers
Event bus
Projections
Read databases

A simple:

Controller
   |
   v
Service
   |
   v
Repository
   |
   v
Database

may be much better.


CQRS Is Not a Microservices Pattern

CQRS can exist inside:

Monolith

or:

Modular Monolith

or:

Microservices

or:

Distributed Event-Driven System

Think of CQRS as a modeling and architectural pattern.

Do not automatically equate:

CQRS
=
Microservices

They are independent decisions.


CQRS Is Not an Architecture for Everything

One of the most important lessons is:

CQRS should usually be applied selectively, not blindly across the entire system.

You might have:

Order Management
    |
    +-- CQRS ✓

User Profile
    |
    +-- CRUD ✓

Admin Settings
    |
    +-- CRUD ✓

Reporting
    |
    +-- Read-optimized model ✓

There is no requirement for every subsystem to use the same architecture.

Microsoft's CQRS journey guidance similarly emphasizes that CQRS is not intended to be automatically imposed as the top-level architecture for an entire system.


A Good Evolution Path

Do not start with:

Microservices
Kafka
CQRS
Event Sourcing
Multiple Databases
Kubernetes

just because the system might eventually need them.

Start simple.

Stage 1 - CRUD

API
 |
 v
Service
 |
 v
Database

Stage 2 - Separate Code Responsibilities

Commands
Queries

but:

Same Database

Stage 3 - Optimize Read Model

Introduce:

Read-specific DTOs
Native SQL
Denormalized tables
Indexes

Stage 4 - Separate Read Storage

Only if required:

Write DB
   |
   v
Read DB

Stage 5 - Introduce Events

Write DB
   |
   v
Outbox
   |
   v
Broker
   |
   v
Projection
   |
   v
Read DB

Stage 6 - Consider Event Sourcing

Only if the domain actually benefits from:

Event history
Replay
Auditability
Temporal reconstruction
Multiple projections

This incremental approach keeps complexity proportional to the actual problem.


CQRS + Saga + Outbox

Now we can connect the patterns from this series.

Suppose an order requires:

Create Order
Reserve Payment
Reserve Inventory
Arrange Shipment

A Saga coordinates the business workflow:

              Saga
                |
                v
          Create Order
                |
                v
         Reserve Payment
                |
                v
       Reserve Inventory
                |
                v
         Arrange Shipment

CQRS separates:

Commands

from:

Queries

Outbox reliably publishes state changes:

Command
   |
   v
Write Model
   |
   +---- State
   |
   +---- Outbox
           |
           v
         Broker
           |
           v
      Other Services

And projections build optimized read models:

Events
   |
   +---- Order Dashboard
   +---- Customer History
   +---- Sales Report

So the complete architecture can become:

                         Client
                           |
                 +---------+---------+
                 |                   |
                 v                   v
             Commands             Queries
                 |                   |
                 v                   v
           Command Model         Query Model
                 |                   |
                 v                   v
             Write DB             Read DB
                 |
               Outbox
                 |
                 v
              Broker
                 |
       +---------+---------+
       |         |         |
       v         v         v
    Payment   Inventory  Shipping
       |
       v
    Events
       |
       v
   Projections
       |
       v
    Read DB

Now each pattern has a clear responsibility.

CQRS
    ↓
Separates commands and queries

Saga
    ↓
Coordinates distributed business workflow

Outbox
    ↓
Reliably publishes local state changes

Broker
    ↓
Transports events

Projection
    ↓
Builds optimized read models

Idempotency
    ↓
Makes duplicate delivery safe

This is much more useful than memorizing the names of the patterns.


A Production Failure Walkthrough

Let's see what happens when things go wrong.

Command Database Fails

CreateOrder
    |
    v
Write DB
    |
    X
Failure

The command fails.

No order should be considered committed.


Write Succeeds but Publisher Is Down

Write DB ✓
Outbox ✓
     |
     X
Publisher down

The read model may not update.

But the event remains durable.

When the publisher returns:

Outbox
   |
   v
Publisher
   |
   v
Broker

the event is eventually delivered.


Broker Is Down

Outbox
   |
   v
Publisher
   |
   X
Broker unavailable

The publisher retries.

The write side does not necessarily need to be unavailable.


Projector Is Down

Broker
   |
   X
Projector

The write model remains operational.

The read model becomes stale.

When the projector recovers:

Broker
   |
   v
Projector
   |
   v
Read DB

it catches up.


Duplicate Event

OrderCreated
OrderCreated

The projector checks:

eventId

and safely ignores the duplicate.


Read Database Is Down

Queries
   |
   X
Read DB

The command side may still be able to accept writes.

Whether the application can tolerate this depends on the product requirements.

This is one of the major architectural benefits of separating the two sides:

Read failure

does not necessarily imply:

Write failure

The Deepest Mental Model

Traditional CRUD asks:

"What database object am I manipulating?"

CQRS asks:

"What is the user trying to do, and what information does the user need?"

That changes the architecture.

Instead of:

OrderController
      |
      v
OrderService
      |
      v
OrderEntity

you begin thinking in terms of:

Commands

PlaceOrder
CancelOrder
ShipOrder
RefundOrder

and:

Queries

GetOrder
GetCustomerOrders
GetOrderDashboard
GetSalesReport

The write model is designed around:

Business behavior.

The read model is designed around:

Information consumption.

That is the essence of CQRS.


The Three Levels of CQRS

It is useful to think of CQRS as having levels.

Level 1 - Interface Separation

Commands
Queries

with separate handlers.

Same database.

                 Application
                /           \
          Commands         Queries
              |               |
              v               v
        Command Model     Query Model
                \           /
                 \         /
                  Database

This is the simplest form.


Level 2 - Model Separation

Now:

Write Model

and:

Read Model

have genuinely different structures.

Write Model
    |
    v
Normalized Domain Data

Read Model
    |
    v
Denormalized Query Data

Still potentially one database.


Level 3 - Storage Separation

Now:

Write Model
    |
    v
Write Database

and:

Read Model
    |
    v
Read Database

with:

Events

synchronizing them.

This provides the greatest flexibility but also introduces the greatest complexity.


CQRS Is a Spectrum

Therefore, CQRS is not:

ON

or:

OFF

It is better understood as a spectrum.

Simple CRUD
    |
    v
Separate Commands/Queries
    |
    v
Separate Models
    |
    v
Separate Read Projections
    |
    v
Separate Databases
    |
    v
Event-driven CQRS
    |
    v
CQRS + Event Sourcing

You should stop at the level that solves your actual problem.


Final Architecture

A mature CQRS system might look like:

                              Client
                                |
                    +-----------+-----------+
                    |                       |
                    v                       v
                Commands                 Queries
                    |                       |
                    v                       v
             Command Handler          Query Handler
                    |                       |
                    v                       v
              Domain Model             Read Model
                    |                       |
                    v                       v
              Write Database           Read Database
                    |
                    v
                 Outbox
                    |
                    v
              Message Broker
                    |
          +---------+---------+
          |                   |
          v                   v
     Projection A        Projection B
          |                   |
          v                   v
     Read Database        Search Index

And the consistency model becomes:

Command
   |
   v
Write Model
   |
   v
Committed State
   |
   v
Event
   |
   v
Projection
   |
   v
Read Model

There may be a small period where:

Write Model = new state
Read Model  = old state

That is the price of asynchronous read modeling.

The architecture is useful when the benefits outweigh that complexity.


Final Takeaway

CQRS is fundamentally about one idea:

The model that changes state does not have to be the same model that reads state.

Traditional CRUD uses:

One model
    |
    +-- Read
    +-- Write

CQRS separates them:

Command Model
    |
    +-- Business rules
    +-- Validation
    +-- Transactions
    +-- State changes

Query Model
    |
    +-- DTOs
    +-- Read optimization
    +-- Search
    +-- Reporting
    +-- Presentation

The simplest CQRS architecture can still be:

One application
One database
Two models

You can then evolve toward:

Separate read model
        |
        v
Separate read database
        |
        v
Event-driven projections

when the system actually needs it.

And this is where the patterns in this series connect:

CQRS
  |
  | separates
  v
Commands and Queries
  |
  | write changes produce events
  v
Transactional Outbox
  |
  | reliably publishes
  v
Message Broker
  |
  | distributes
  v
Read Model Projectors
  |
  | build
  v
Optimized Query Models

If the system is a distributed business workflow:

Saga
  |
  v
Coordinates the workflow

while:

Outbox
  |
  v
Reliably publishes each local transition

and:

Idempotency
  |
  v
Makes duplicate delivery safe

The most important lesson is therefore not:

"Use CQRS because it scales."

It is:

Separate responsibilities when the read side and write side have genuinely different problems to solve.

If your application is simple CRUD, keep it simple.

If your write model needs strong business invariants while your read side needs completely different representations, queries, scaling characteristics, or storage technologies, CQRS can provide a powerful separation.

And if you combine CQRS with:

Transactional Outbox
+
Event-driven projections
+
Idempotent consumers

you get a robust foundation for building highly scalable event-driven systems.

But CQRS itself remains the simple idea underneath all of it:

              STATE CHANGE
                   |
                   v
               COMMAND
                   |
                   v
              WRITE MODEL

                INFORMATION
                     |
                     v
                  QUERY
                     |
                     v
                 READ MODEL

Different questions deserve different models.