CQRS Pattern
CQRS (Command Query Responsibility Segregation) separates write model (commands, normalized, strict) from read model (queries, denormalized, fast).
Introduction
CQRS (Command Query Responsibility Segregation) separates write model (commands, normalized, strict) from read model (queries, denormalized, fast). One schema serving both compromises performance and complexity — dashboards need JOIN-free reads; writes need aggregate integrity.
The story
An e-commerce dashboard query "customer orders with totals + status + carrier" timed out at 800ms — 40 JOINs on normalized write schema. CQRS with denormalized read store (materialized view + Redis) dropped p95 to 45ms while write-side strictness preserved.
The business problem
Single model for read and write satisfies neither workload:
- Slow dashboards: complex JOINs on normalized write schema.
- Lock contention: reporting queries block writes.
- Compromised design: denormalize writes or over-normalize reads.
- Scale mismatch: reads 100× writes but same DB tuning.
The problem teams faced
CQRS fits when:
- Read and write workloads have incompatible optimal schemas.
- Read latency requirements can't be met by write DB alone.
- Different scaling needed for reads vs writes.
- Complex dashboards/projections from same domain events.
Understanding the topic
Intent: Separate models for updating (commands) and reading (queries).
- Command side — aggregates, validation, domain events, normalized store.
- Query side — denormalized read models, caches, search indexes.
- Sync mechanism — domain events, projections, materialized views.
- Not mandatory: separate databases — can be same DB different tables/views.
Internal architecture
CQRS write/read paths:
// Write sideclass PlaceOrderHandler {async handle(cmd: PlaceOrder) {const order = Order.create(cmd)await this.writeRepo.save(order)await this.events.publish(order.toDomainEvents())}}// Read side — projectoron(OrderPlaced, async (e) => {await readDb.upsert("order_dashboard", {orderId: e.id, customer: e.customer, total: e.total, status: "placed"})})// Query — no JOINsGET /dashboard/orders → readDb.query("SELECT * FROM order_dashboard WHERE ...")
Visual explanation
Three diagrams cover CQRS split, projection flow, and when to adopt:
Informative example
Before / after — JOIN-heavy query to read model:
// ❌ 40 JOINs on write schema — 800ms p95SELECT o.*, c.name, s.carrier, SUM(i.price) ...FROM orders o JOIN customers c JOIN shipments s JOIN items i ...WHERE o.customer_id = ?// ✅ Denormalized read model — 45ms p95// Updated by projector on OrderPlaced / OrderShipped eventsSELECT * FROM order_dashboard WHERE customer_id = ?// Write side unchanged — normalized aggregate integrity
Execution workflow
Measure read pain
Slow queries, lock contention — profile first.
Real-world use
Materialized views in Postgres, Elasticsearch projections, Redis read caches, EventStoreDB projections, NestJS CQRS module, Axon query side.
Production case study
E-commerce dashboard CQRS:
- Symptom: 800ms p95, 40 JOINs on write DB.
- Decision: order_dashboard read table + Redis cache; project from events.
- Outcome: p95 45ms; write model stayed normalized.
Trade-offs
- Pro: optimized read and write independently; scale separately.
- Con: eventual consistency on read side; dual model maintenance.
- Con: complexity — only worth measured read pain.
Decision framework
- Use when read queries can't be optimized on write schema.
- Use when read:write ratio extreme and scale differs.
- Start with materialized view same DB — not separate micro-DB day one.
- Avoid CQRS on simple CRUD with fast queries.
Best practices
- Commands return IDs/status — not full denormalized graphs.
- Version read model projections — replay events on schema change.
- Monitor projection lag — alert when stale reads unacceptable.
Anti-patterns to avoid
- CQRS everywhere on CRUD app — operational burden without benefit.
- Read model updated synchronously in command handler — couples sides.
- Different read/write without event/version strategy — drift bugs.
Common mistakes
- User sees stale dashboard for seconds after action — UX consideration.
- Projection bugs cause silent read/write divergence — reconciliation jobs needed.
Debugging tips
- Compare write aggregate vs read projection for same ID — find projector bugs.
- Metric: projection lag milliseconds.
Optimization strategies
- Redis cache on hottest read queries after materialized view.
Common misconceptions
- CQRS ≠ Event Sourcing. Often paired but independent — CQRS can use snapshot DB on write side.
- CQRS doesn't require separate microservices — in-process split OK.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionCQRS vs CRUD same model?+
Answer
Follow-up
2AdvancedQuestionEventual consistency UX?+
Answer
Follow-up
3IntermediateQuestionCQRS without Event Sourcing?+
Answer
Follow-up
Summary
You can split read/write models, build projections, and explain eventual consistency trade-offs. Tell the 800ms→45ms dashboard story — if you can do that, you own CQRS.