CQRS Pattern: Separating Write Models from Query Models at Scale
Why CQRS Exists: Protect Write Truth While Shaping Fast Reads
CQRS works when read and write workloads diverge, but only with explicit freshness budgets and projection reliability.
π― System Scenario & Requirements
System Goal
CQRS works when read and write workloads diverge, but only with explicit freshness budgets and projection reliability.
Actors & Components
- Client Layer
- Core Processing
- Eventing & Messaging
- Data & Persistence
- Web Client
- Order Service (Write Model)
- PostgreSQL (Write Store)
- Event Stream (Outbox)
Functional Requirements
- TLDR: CQRS works when read and write workloads diverge, but only with explicit freshness budgets and projection reliability.
- The hard part is not separating models β it is operating lag, replay, and rollback safely.
- An e-commerce platform's order service was running 47-table JOINs to serve its summary page β because reads and writes shared the same normalized model.
Constraints & Assumptions
- CQRS is worth the cost when read workloads are truly different.
- If the only goal is maybe faster later, teams usually end up with more systems and the same old ambiguity.
β‘ Execution Stages & Decision Trace
Stage 1 β The Boundary Model: Command Side, Event Stream, Projection Side
At a practical level, CQRS is not just two databases.
It is a contract about where truth is written and how derivative views are built.
Deep Dive
Scenario context
CQRS works when read and write workloads diverge, but only with explicit freshness budgets and projection reliability. The hard part is not separating models β it is operating lag, replay, and rollback safely.
Observable outcome: The critical visual detail is that the outbox or CDC mechanism sits at the boundary between the write and read subgraphs, making the write-to-read propagation explicit and decoupled.
Stage 2 β How the Write Path and Read Path Stay Separate
- A healthy CQRS flow looks boring on purpose: 1.
- The command handler validates the requested state change against current write-side rules.
- The transaction commits the write and records an outbox event or change record in the same durability boundary.
- A relay publishes that committed event to projection workers.
sequenceDiagram
participant C as Client
participant CA as Command API
participant CH as Command Handler
participant WS as Write Store
participant OB as Outbox
participant PW as Projection Worker
C->>CA: POST /orders (command)
CA->>CH: validate and handle
CH->>WS: commit to write store
WS->>OB: record outbox event (atomic)
OB->>PW: relay committed event
PW->>PW: update read model with checkpoint
CA-->>C: 202 Accepted
sequenceDiagram
participant C as Client
participant QA as Query API
participant RM as Read Model
participant QS as Query Store
C->>QA: GET /orders/timeline
QA->>RM: query read model
RM->>QS: fetch from query store
QS-->>RM: projection data
RM-->>QA: shaped response
QA-->>C: timeline response (may lag write by seconds)
Observable outcome: A relay publishes that committed event to projection workers.
Stage 3 β Deep Dive: Lag, Replay, and Projection Safety
- The Internals: Write Authority, Checkpoints, and Read Staleness The write side is the only place where business invariants should be enforced.
- Projection workers are downstream materializers; they should never be asked to resolve conflicts that belong to the command model.
- A replayed projection should rebuild a view from committed events, not guess what the latest truth is.
- Operators usually need three durable markers: - a source-of-truth commit version or event ID, - a per-projection checkpoint, - a freshness budget that maps lag into user impact.
Observable outcome: Operators usually need three durable markers: - a source-of-truth commit version or event ID, - a per-projection checkpoint, - a freshness budget that maps lag into user impact.
Stage 4 β Operator Field Note: Freshness Budgets Fail Before Correctness Does
- In incident reviews, the first visible symptom of CQRS trouble is usually outdated reads, not corrupted writes.
- Support tickets say a status is old long before anyone proves the write path is wrong.
- Operators usually find that the most valuable architecture review artifact is a freshness table per read surface: acceptable lag, pager threshold, and replay procedure.
Observable outcome: Operators usually find that the most valuable architecture review artifact is a freshness table per read surface: acceptable lag, pager threshold, and replay procedure.
Stage 5 β CQRS Flow: Commit Once, Project Many
- This flowchart shows the full CQRS data pipeline from a single command API entry point through transactional write, outbox relay, and projection worker to multiple specialized query stores.
- Two additional outputs β a committed version token returned to the read path and a projection checkpoint β make the consistency and replay properties of the pipeline explicit.
- The key takeaway is that a single committed write fans out to as many read models as the system needs, each optimized independently, all derived from the same immutable source of truth in the write store.
flowchart TD
A[Command API] --> B[Validate business rule]
B --> C[Transactional write store]
C --> D[Outbox or change stream]
D --> E[Projection worker]
E --> F[Query store]
F --> G[API or UI read path]
E --> H[Projection checkpoint]
C --> I[Committed version token]
I --> G
Observable outcome: The key takeaway is that a single committed write fans out to as many read models as the system needs, each optimized independently, all derived from the same immutable source of truth in the write store.
Stage 6 β Practical Example: Order Service With Axon Framework
- The order scenario from the previous section maps directly to Axon's programming model.
- Commands flow in through CommandGateway , the aggregate persists events to the event store, and the projection worker builds the timeline read model asynchronously.
- Maven dependency Commands and queries as plain records Commands carry intent; queries carry selection criteria.
- Write side: aggregate enforces invariants, never touches the read store AggregateLifecycle.apply() commits the event to the Axon event store.
flowchart LR
A[REST POST /orders] --> B[CommandGateway]
B --> C[OrderAggregate]
C --> D[EventStore]
D --> E[EventBus]
E --> F[@EventHandler Projection Worker]
F --> G[QueryStore]
G --> H[QueryGateway]
H --> I[REST GET /timeline]
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>4.9.3</version>
</dependency>
Observable outcome: Write side: aggregate enforces invariants, never touches the read store AggregateLifecycle.apply() commits the event to the Axon event store.
End-to-end outcome
After working through the stages, you should be able to explain:
- CQRS is a control boundary between write truth and read convenience, not a blanket microservices rule.
- Freshness budgets and replay tooling matter as much as the schema design.
Alternatives considered
Decision Guide: When CQRS Earns Its Complexity
Start with one domain that already hurts, such as orders or billing, and prove that lag, replay, and recovery are manageable before expanding the pattern elsewhere.
Failure and degraded modes
Trade-offs & Failure Modes: Trade-offs and Failure Modes
- Detection: CQRS is worth the cost when read workloads are truly different.
- User or operator impact: CQRS is worth the cost when read workloads are truly different.
