Skip to content

Architecture Lab

CQRS Explained

When and how to separate read and write models.

Many applications begin with one data model for everything:

  • Creating and updating business records
  • Validating rules
  • Displaying dashboards
  • Searching
  • Generating reports
  • Serving mobile applications
  • Exporting data
  • Running analytics

This approach is often sufficient in the early stages of a system.

As the product grows, however, the write side and the read side begin to demand very different things.

The write side needs:

  • Transactional consistency
  • Business-rule validation
  • Concurrency control
  • Carefully structured data
  • Small and predictable updates

The read side may need:

  • Fast filtering and sorting
  • Full-text search
  • Denormalized views
  • Aggregated dashboards
  • Data combined from several domains
  • Independent scaling

One shared model eventually becomes a compromise.

 

Figure 1: A single model must satisfy competing write and read requirements.

A normalized schema that protects business consistency may require several joins to answer a dashboard query. A model optimized for reporting may be unsuitable for enforcing transactional rules.

CQRS addresses this tension by separating the responsibility for changing state from the responsibility for reading state.

What is CQRS?

CQRS stands for Command Query Responsibility Segregation.

It separates an application into two conceptual paths:

Command side

The command side handles requests that change state.

Examples include:

  • Create Order
  • Confirm Payment
  • Cancel Reservation
  • Approve Payout
  • Change Delivery Address

A command represents business intent.

It asks the system to perform an action and may succeed or fail depending on business rules.

Query side

The query side retrieves information without changing business state.

Examples include:

  • Get Order Summary
  • Search Customers
  • List Failed Payments
  • Display Payout Dashboard
  • Retrieve Customer Order History

A query should return information without producing business side effects.


Figure 2: The command side owns state changes, while the query side serves task-specific views.

The write model remains responsible for authoritative business decisions. Changes from the write side are used to update one or more read models.

The read and write paths may use:

  • Separate handlers over the same database
  • Separate models over the same schema
  • Separate database schemas
  • Completely different storage technologies
  • Synchronous or asynchronous synchronization

CQRS does not automatically mean two databases, microservices, Kafka, or event sourcing.

The separation can begin entirely inside one application.

CQRS in an e-commerce order system

Consider an order-management platform.

The command side may support operations such as:

  • Create an order
  • Add an item
  • Confirm an order
  • Cancel an order

The command model must protect rules such as:

  • A cancelled order cannot be confirmed.
  • The order total must match its line items.
  • A confirmed order cannot accept arbitrary item changes.
  • Only an authorized user may cancel the order.
  • Duplicate confirmation requests must not create duplicate effects.

The query side serves different user experiences:

  • Checkout confirmation
  • Customer order history
  • Support-agent search
  • Operations dashboard
  • Sales reporting

These views do not need to resemble the write model.

Figure 3: One order aggregate can publish changes that feed several independently shaped read models.

For example, the customer order-history view might contain:

Order ID
Order date
Payment status
Shipment status
Total amount
Expected delivery

The support view may include:

Order ID
Customer identity
Payment attempt history
Fulfilment state
Refund status
Risk classification
Last support contact

Both represent the same order, but they answer different questions.

Trying to force both through the same domain object and query path often produces complex joins, overloaded APIs, and brittle models.

Commands express business intent

A command should describe why the state is changing.

Prefer:

Confirm Order
Cancel Reservation
Capture Payment
Approve Refund
Change Delivery Address

Avoid overly generic commands such as:

Update Order
Update Payment
Update Customer

Generic update operations expose storage mechanics but hide business meaning.

A well-designed command makes it easier to determine:

  • Who is allowed to perform the operation
  • Which rules apply
  • Which fields may change
  • Which event should be produced
  • How the operation should be audited
  • Whether the operation is idempotent

The command-processing path

A typical command path contains several stages.

  1. Receive the command.
  2. Authenticate the caller.
  3. Authorize the requested action.
  4. Load the relevant aggregate or business state.
  5. Validate business invariants.
  6. Apply the transition.
  7. Commit the state change atomically.
  8. Publish a domain event safely.
  9. Return a minimal result.

Figure 4: The command path validates business intent before committing a state transition.

Suppose the system receives:

Confirm Order

The command handler may verify:

  • The order exists.
  • The order is not already cancelled.
  • The order contains at least one item.
  • Pricing is still valid.
  • Required inventory is available.
  • The caller is authorized.
  • The same confirmation operation has not already succeeded.

If any invariant fails, the command is rejected without changing state.

Command results

A command usually returns only what the caller needs to continue.

For example:

{
  "orderId": "order-9014",
  "status": "CONFIRMED",
  "version": 8
}

The command does not need to return every field required by every future screen.

Those richer representations belong to the query side.

The write model

The write model represents business behaviour and authoritative state transitions.

It may contain:

  • Aggregates
  • Entities
  • Value objects
  • Domain services
  • Validation policies
  • Authorization rules
  • Concurrency controls
  • Idempotency records

The write model should answer questions such as:

  • Is this transition valid?
  • What must change atomically?
  • Which invariant must always remain true?
  • What event represents the completed change?
  • What should happen when two commands race?

The write side is not necessarily optimized for broad reporting or search.

That is intentional.

Concurrency on the command side

Separating reads and writes does not remove concurrent-update problems.

Two commands may attempt to modify the same aggregate.

Examples:

  • Two users attempt to reserve the final item.
  • Cancellation races with fulfilment.
  • Two agents approve the same refund.
  • A customer submits the same payment twice.
  • Two administrators update the same policy.

Common controls include:

  • Optimistic locking
  • Entity versions
  • Compare-and-set updates
  • Database constraints
  • Idempotency keys
  • Scoped business locks

A command should generally include or derive the expected state version.

For example:

Confirm Order
Order ID: order-9014
Expected Version: 7

If the stored order is already at version 8, the command should reload and re-evaluate rather than silently overwriting newer state.

From write changes to read models

When the write model changes, the query side must learn about the change.

Common change sources include:

  • Domain events
  • Transactional outbox events
  • Change-data capture
  • Database transaction logs
  • Synchronous application updates
  • Database views

For distributed CQRS systems, domain events and transactional outbox patterns are common.

A possible event is:

{
  "eventId": "evt-7721",
  "eventType": "OrderConfirmed",
  "orderId": "order-9014",
  "orderVersion": 8,
  "occurredAt": "2026-07-31T09:30:00Z"
}

A projection consumes that event and updates one or more read models.

Projection pipeline

A projection transforms authoritative changes into query-optimized representations.

Figure 5: One event stream can update several read models designed for different consumers.

For example:

Order Confirmed
      |
      +--> Update customer order summary
      |
      +--> Update support search index
      |
      +--> Update sales dashboard
      |
      +--> Update fulfilment work queue

Each read model may use a different technology.

Examples:

  • PostgreSQL for operational views
  • Elasticsearch or OpenSearch for search
  • Redis for low-latency lookups
  • A document database for denormalized records
  • A warehouse for analytics

The technology should match the query workload.

Projection requirements

A production projection must handle more than the happy path.

Idempotency

The same event may be delivered more than once.

Applying it repeatedly should not corrupt the read model.

Possible techniques include:

  • Store processed event IDs
  • Store the latest entity version
  • Use upsert operations
  • Make projection updates naturally idempotent

Ordering

Events for one entity may arrive late or out of order.

Suppose the projection receives:

Order Version 8
Order Version 7

It should not overwrite version 8 with stale version 7.

Store the latest applied version and reject older updates.

Rebuildability

A read model may become corrupted or need a new schema.

The system should define how it can be rebuilt.

Possible approaches:

  • Replay retained domain events
  • Re-run change-data capture history
  • Reconstruct from the write database
  • Run a controlled backfill job

A projection that cannot be rebuilt requires another explicit repair strategy.

Schema evolution

Read-model schemas will change over time.

A safe migration may involve:

  1. Create a new projection version.
  2. Backfill it from history.
  3. Compare old and new results.
  4. Redirect queries to the new view.
  5. Remove the old model after validation.

Eventual consistency

When read models are updated asynchronously, the command may complete before the query model is current.

This creates eventual consistency.

For example:

10:00:00.000  Order confirmed
10:00:00.040  Write transaction committed
10:00:00.120  OrderConfirmed event published
10:00:00.350  Projection updated

For approximately 350 milliseconds, the read model may show the previous state.

The important engineering question is not:

Is the system eventually consistent?

It is:

How stale may the read model become, and how will that staleness be measured?

Define measurable objectives such as:

  • 99% of order projections update within two seconds.
  • No support view remains more than 30 seconds behind.
  • Critical payment projections trigger an alert after five seconds.
  • A rebuilding projection exposes its current progress.

Useful metrics include:

  • Projection lag
  • Oldest unprocessed event
  • Event processing latency
  • Failed projection count
  • Replay progress
  • Stale-record count

Read-after-write consistency

A user may expect to see a change immediately after submitting it.

Suppose a customer updates a delivery address and then opens the order page.

The command may have succeeded, but the read model may still show the previous address.

There are several practical strategies.

Figure 6: Different operations can use different read-after-write strategies.

1. Return updated fields from the command

The command response returns the minimum updated information required by the initiating interface.

Example:

{
  "orderId": "order-9014",
  "deliveryAddress": "Updated address",
  "version": 9
}

This is useful for the immediate UI, but the command should not become a replacement for every query.

2. Read from the write store temporarily

For a critical immediate read, the application can query the authoritative store.

This preserves stronger consistency but introduces a second read path that must be managed carefully.

3. Wait for a projection version

The command returns version 9.

The query waits until the read model reaches at least version 9 before responding.

This provides an explicit consistency boundary but may increase latency.

4. Use optimistic UI

The client displays the accepted change locally while the projection catches up.

This is appropriate when:

  • The command was accepted definitively
  • The change is easy to represent locally
  • Brief staleness is acceptable
  • Failure correction is supported

5. Accept the delay

For analytics, reporting, recommendations, and non-critical dashboards, visible delay may be acceptable.

The strategy should match the user’s expectation and the consequence of stale information.

The query side

The query side is optimized for retrieving information.

It may use:

  • Direct SQL
  • Database views
  • Denormalized tables
  • Search indexes
  • Materialized views
  • Document stores
  • Caches
  • Precomputed aggregates

A query model should be shaped around a task.

For example:

Get Customer Order History
may return:
{
  "customerId": "customer-224",
  "orders": [
    {
      "orderId": "order-9014",
      "orderDate": "2026-07-31",
      "status": "SHIPPED",
      "total": 4250,
      "currency": "INR"
    }
  ]
}

The query side should not duplicate command-side business rules.

It may:

  • Filter
  • Sort
  • Aggregate
  • Format
  • Paginate
  • Redact unauthorized fields

It should not independently decide whether an order may be cancelled or whether a refund is valid.

Those decisions belong to the command side.

Levels of CQRS adoption

CQRS does not need to begin with a fully distributed architecture.

Level 1: Separate handlers

Commands and queries use different code paths but share the same database and model.

Command Handler → Shared Database
Query Handler   → Shared Database

This improves clarity with minimal operational cost.

Level 2: Separate models over one database

Commands use domain entities.

Queries use direct SQL, database views, or separate response models.

Command Model → Shared Database
Query Model   → Shared Database

This is often sufficient for applications with complex writes and rich reads.

Level 3: Separate schemas or stores

The write side and read side use distinct schemas or databases.

Changes are synchronized through projections.

Write Store → Events → Projection → Read Store
This enables independent scaling and specialized storage.

Level 4: CQRS with event sourcing

The write side stores events as the source of truth.

Read models and current state are derived from those events.

This is the most operationally demanding form and should be adopted only when its benefits are required.

CQRS is not event sourcing

CQRS and event sourcing are related but different patterns.

Figure 7: CQRS separates command and query responsibilities; event sourcing changes how authoritative state is stored.

CQRS decides:

  • How write responsibilities are separated from read responsibilities
  • How command and query models differ
  • How read workloads are served

Event sourcing decides:

  • That state changes are stored as immutable events
  • That current state is reconstructed from event history
  • That the event stream is the authoritative source of truth

CQRS can be implemented without event sourcing.

For example:

Commands update a normal relational database.
An outbox publishes domain events.
Projections update a search index.

That is CQRS without event sourcing.

Event-sourced systems commonly use CQRS because event streams are better suited to writes and projections are better suited to reads.

But the patterns should be evaluated separately.

Benefits of CQRS

Clearer responsibilities

The command side focuses on valid state transitions.

The query side focuses on serving information.

Better read performance

Read models can be denormalized and designed around actual user tasks.

Independent scaling

Read-heavy systems can scale the query side without scaling write processing equally.

Specialized storage

Search, reporting, and low-latency lookups can use technologies suited to their workloads.

Stronger domain modelling

The write model can focus on business language and invariants instead of UI requirements.

Multiple read models

The same domain change can feed:

  • Customer views
  • Support tools
  • Operational dashboards
  • Search indexes
  • Analytics

Reduced runtime aggregation

Precomputed views reduce the need for a UI or backend-for-frontend to call many services synchronously.

Costs and trade-offs

Eventual consistency

The read side may temporarily lag behind authoritative state.

More models

One business concept may appear as:

  • A command
  • A write entity
  • A domain event
  • A projection
  • A read DTO

More infrastructure

Separate stores and event pipelines require:

  • Deployment
  • Monitoring
  • Backup
  • Replay
  • Capacity planning
  • Failure handling

More difficult debugging

An incorrect query result may originate from:

  • Command processing
  • Event publication
  • Projection logic
  • Ordering
  • Schema evolution
  • Stale data

Dual-write risk

If a write transaction and event publication are separate, one can succeed while the other fails.

Use a transactional outbox or equivalent reliable publication mechanism.

Operational responsibility

Teams need tools to:

  • Inspect projection lag
  • Replay events
  • Repair read models
  • Quarantine failures
  • Compare read and write state

CQRS relocates complexity. It does not remove it.

When to use CQRS

Figure 8: CQRS is valuable when write integrity and read requirements genuinely diverge.

CQRS is a good fit when:

  • Write-side business rules are complex.
  • Reads are much more frequent than writes.
  • Read and write workloads scale differently.
  • The product needs several representations of the same data.
  • Search and reporting require specialized models.
  • The user experience tolerates measurable projection delay.
  • Auditability and domain events already have value.
  • Runtime aggregation across services creates latency or fragility.

Examples include:

  • Payments and payouts
  • Marketplace order processing
  • Reservation systems
  • Inventory platforms
  • Financial ledgers
  • Operational dashboards
  • Customer-support views
  • Workflow systems
  • Large searchable catalogues

When not to use CQRS

Avoid CQRS when:

  • The application is straightforward CRUD.
  • One model serves both reads and writes well.
  • Every read requires immediate strong consistency.
  • The team cannot operate event pipelines and projections.
  • A database index solves the performance problem.
  • A read replica is sufficient.
  • A materialized view provides the required query shape.
  • A cache provides the required latency.
  • The additional models would exceed the business value.

A simpler architecture is not less professional.

The correct architecture is the smallest one that satisfies the system’s requirements.

Security considerations

Separating reads and writes creates different security concerns.

Command-side security

Commands require:

  • Authentication
  • Authorization
  • Business-rule validation
  • Idempotency
  • Audit logging
  • Concurrency protection
  • Replay protection where necessary

Query-side security

Queries require:

  • Tenant isolation
  • Field-level permissions
  • Data redaction
  • Pagination limits
  • Safe filtering
  • Protection from bulk data extraction

A denormalized read model may contain data collected from several domains.

That convenience can accidentally widen access.

Read models should store only the information needed for their purpose.

Observability

A production CQRS system should expose both technical and business health.

Command metrics

  • Command success rate
  • Business-rejection rate
  • Validation failures
  • Concurrency conflicts
  • Command latency
  • Idempotent-retry count

Event metrics

  • Events published
  • Publication failures
  • Outbox backlog
  • Duplicate events
  • Event age

Projection metrics

  • Projection lag
  • Processing latency
  • Failed events
  • Rebuild progress
  • Stale entities
  • Version gaps

Query metrics

  • Query latency
  • Query error rate
  • Cache-hit rate
  • Search latency
  • Result-size distribution

The most useful metric is often the time from accepted command to visible read-model update.

Testing CQRS systems

Command-side tests

Test:

  • Business invariants
  • Authorization
  • Invalid state transitions
  • Duplicate commands
  • Concurrency conflicts
  • Transaction boundaries
  • Domain-event creation

Projection tests

Test:

  • Duplicate events
  • Out-of-order events
  • Missing versions
  • Schema changes
  • Projection restarts
  • Full rebuilds
  • Partial failures

Query-side tests

Test:

  • Correct data shape
  • Tenant isolation
  • Filtering and pagination
  • Data redaction
  • Stale-read behaviour

End-to-end tests

Verify:

Command accepted
      ↓
Write committed
      ↓
Event published
      ↓
Projection updated
      ↓
Query reaches expected version

Also test what happens when each stage fails.

Design review checklist

Before introducing CQRS, ask:

  • What specific problem does CQRS solve?
  • Can separate handlers over one database solve it?
  • Which model is authoritative?
  • How are changes propagated?
  • How is the dual-write problem handled?
  • What projection lag is acceptable?
  • How is read-after-write handled?
  • Can read models be rebuilt?
  • How are duplicates and ordering handled?
  • Which business rules remain on the command side?
  • How will operators inspect and repair projections?
  • Is the added operational cost justified?

Key trade-offs

Benefits Costs
Command model protects business invariants More models and components
Read models are optimized for user tasks Eventual consistency
Reads and writes can scale independently Projection monitoring is required
Different storage technologies can be used Schema evolution becomes more involved
Multiple views can be built from the same changes Debugging spans several stages
Runtime aggregation can be reduced Replay and repair tooling are needed
Domain intent becomes clearer Too much separation can over-engineer simple systems

Final perspective

CQRS is not primarily about using two databases.

It is about recognizing that changing business state and answering user questions are different responsibilities.

The command side should represent authoritative business truth.

The query side should represent information in the form each consumer needs.

The pattern is valuable when those responsibilities genuinely diverge.

Used carefully, CQRS can improve clarity, scalability, and performance.

Used without a real need, it replaces a simple model with unnecessary operational complexity.

The strongest implementation principle is:Use the smallest degree of CQRS that solves the actual problem.

Back to Architecture Lab