CQRS Implementation Guide: Separating Commands from Queries
Most applications use a single model to read and write data. That works until read and write requirements diverge — complex queries slow down writes, or write validation logic pollutes read paths. Command Query Responsibility Segregation (CQRS) solves this by splitting a system into two distinct models: one optimized for writes and another optimized for reads.
The Core Idea#
CQRS traces back to Bertrand Meyer's Command-Query Separation (CQS) principle: a method should either change state (command) or return data (query), never both. CQRS elevates that principle to the architectural level.
┌──────────────┐ ┌───────────────────┐
│ Client │────────▶│ Command Side │
│ │ │ (Write Model) │
└──────────────┘ │ - Validates │
│ │ - Enforces rules │
│ │ - Persists events │
│ └────────┬──────────┘
│ │ events
│ ┌────────▼──────────┐
│ │ Projection / │
└────────────────▶│ Query Side │
│ (Read Model) │
│ - Denormalized │
│ - Fast lookups │
└───────────────────┘
Commands express intent — PlaceOrder, CancelSubscription, ApproveRefund. Queries express questions — GetOrderHistory, ListActiveSubscriptions. Each side can scale, deploy, and evolve independently.
Separate Read and Write Models#
Write Model#
The write model owns domain logic and invariants. It validates commands, applies business rules, and persists state transitions. It does not care about how data will be displayed.
class OrderAggregate:
def handle_place_order(self, cmd: PlaceOrder):
if self.status != "draft":
raise InvalidOrderState("Order already placed")
if cmd.total <= 0:
raise ValidationError("Order total must be positive")
self.apply(OrderPlaced(order_id=cmd.order_id, total=cmd.total))
def on_order_placed(self, event: OrderPlaced):
self.status = "placed"
self.total = event.total
Read Model#
The read model is a denormalized projection tailored to specific query needs. It can be a flat table, a materialized view, a search index, or even a graph.
-- Projection: order_summary (optimized for dashboard)
CREATE TABLE order_summary (
order_id UUID PRIMARY KEY,
customer TEXT,
status TEXT,
total DECIMAL,
placed_at TIMESTAMP
);
Because the read model is derived, you can rebuild it from scratch whenever the schema changes — no migrations required.
Event Sourcing + CQRS#
CQRS pairs naturally with event sourcing, where the write model persists a sequence of domain events rather than current state.
Command ──▶ Aggregate ──▶ Event(s) ──▶ Event Store
│
┌──────▼───────┐
│ Projector │
│ (builds read │
│ models) │
└──────┬───────┘
│
┌──────▼───────┐
│ Read DB │
└──────────────┘
Benefits of combining the two:
- Full audit trail — every state change is an immutable event.
- Temporal queries — reconstruct state at any point in time.
- Multiple projections — one event stream feeds dashboards, search, analytics, and notifications simultaneously.
- Replay — rebuild projections or fix bugs by replaying the event log.
Eventual Consistency#
With separate models, the read side lags behind the write side. This is eventual consistency — after a command succeeds, queries may return stale data until the projection catches up.
Strategies to manage this:
- Causal consistency tokens — the write side returns a version token; the client includes it in subsequent reads, and the read side waits until it has processed that version.
- Read-your-own-writes — route the issuing user's next read to the write database (or a synchronous projection) so they see their own change immediately.
- Polling with backoff — the client polls the read model until the expected state appears, with exponential backoff.
- UI optimistic updates — update the UI immediately on command acceptance and reconcile when the projection catches up.
Consistency lag is typically measured in milliseconds to low seconds with well-tuned projections.
When CQRS Is Overkill#
CQRS adds moving parts. Avoid it when:
- Read and write models are nearly identical — a single model with an ORM is simpler.
- Low traffic — the operational complexity is not justified.
- Simple CRUD — if your domain has no complex invariants, a standard repository pattern suffices.
- Small team — CQRS requires discipline around event schemas, projection maintenance, and consistency trade-offs.
A good litmus test: if you find yourself building separate database views or caching layers to speed up reads while your write path has complex validation, CQRS likely pays for itself.
Tooling#
Axon Framework (JVM)#
Axon provides first-class support for CQRS and event sourcing on the JVM. It handles command routing, event storage, saga orchestration, and projection tracking.
@Aggregate
public class OrderAggregate {
@CommandHandler
public void handle(PlaceOrderCommand cmd) {
AggregateLifecycle.apply(new OrderPlacedEvent(cmd.getOrderId(), cmd.getTotal()));
}
@EventSourcingHandler
public void on(OrderPlacedEvent event) {
this.status = "placed";
}
}
EventStoreDB#
EventStoreDB is a purpose-built database for event sourcing. It stores events in immutable streams, supports catch-up subscriptions for projections, and provides built-in optimistic concurrency control.
# Append an event to a stream
curl -X POST https://localhost:2113/streams/order-123 \
-H "Content-Type: application/json" \
-H "ES-EventType: OrderPlaced" \
-d '{"orderId": "123", "total": 99.95}'
Other Notable Tools#
- Marten (.NET) — event sourcing and document DB on PostgreSQL.
- Eventuous (.NET) — lightweight event sourcing library.
- Apache Kafka — serves as a durable event log for CQRS projections at scale.
- Debezium — change data capture from existing databases into event streams.
Implementation Checklist#
- Identify bounded contexts where read and write concerns diverge.
- Define commands and events with explicit schemas (Avro, Protobuf, or JSON Schema).
- Build the write model with domain aggregates that enforce invariants.
- Build projections that subscribe to events and populate read-optimized stores.
- Handle idempotency — projections must tolerate duplicate events.
- Monitor projection lag — alert when the read side falls too far behind.
- Plan for schema evolution — version your events and use upcasters for backward compatibility.
Key Takeaways#
CQRS is not a universal pattern — it is a targeted tool for systems where read and write workloads have fundamentally different shapes. When combined with event sourcing, it unlocks powerful capabilities like full audit trails, temporal queries, and independent scaling. The trade-off is operational complexity: eventual consistency, projection maintenance, and event schema evolution all require deliberate engineering.
Start with a single bounded context, prove the value, and expand from there.
Want to sharpen your system design skills? Explore 322 more articles on Codelit.dev covering distributed systems, architecture patterns, and real-world engineering.
Try these templates
Continue learning
Go deeper on distributed systems
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
Database Internals
Alex Petrov · 2019
Explains B-trees, LSM trees and consensus protocols so you can predict how a datastore will behave.
4.6 (560)#2 in Management Information SystemsBuilding Event-Driven Microservices
Adam Bellemare · 2025
2025 rewrite on event schemas, topic ownership, and what breaks when teams share streams.
4.7 (8)Kindle editionStreaming Systems
Tyler Akidau, Slava Chernyak, Reuven Lax · 2018
Watermarks, windows and exactly-once explained with the diagrams that make late data click.
4.3 (133)Release It!
Michael T. Nygard · 2018
Post-mortems of real outages, then the stability patterns (circuit breaker, bulkhead) that stop them.
4.6 (501)#4 in Client-Server Networking Systems