Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 48

    CQRS Pattern

    CQRS (Command Query Responsibility Segregation) separates write model (commands, normalized, strict) from read model (queries, denormalized, fast).

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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:

    ts
    // Write side
    class PlaceOrderHandler {
    async handle(cmd: PlaceOrder) {
    const order = Order.create(cmd)
    await this.writeRepo.save(order)
    await this.events.publish(order.toDomainEvents())
    }
    }
    // Read side — projector
    on(OrderPlaced, async (e) => {
    await readDb.upsert("order_dashboard", {
    orderId: e.id, customer: e.customer, total: e.total, status: "placed"
    })
    })
    // Query — no JOINs
    GET /dashboard/orders → readDb.query("SELECT * FROM order_dashboard WHERE ...")

    Visual explanation

    Three diagrams cover CQRS split, projection flow, and when to adopt:

    CQRS split
    Command
    PlaceOrder
    Write model
    Normalized
    Domain event
    OrderPlaced
    Read model
    Denormalized
    Commands don't return complex joins — queries hit read model.
    Projection update
    OrderPlaced
    Event
    Projector
    Updates read DB
    Dashboard query
    Single table read
    Eventual lag
    ms–seconds
    Read model lags slightly — acceptable for most dashboards.
    When CQRS earns cost
    JOIN timeout?
    Evidence
    Read >> write?
    Scale split
    Simple CRUD?
    Skip CQRS
    Start same DB
    Views first
    Don't CQRS day one — split when read pain measured.

    Informative example

    Before / after — JOIN-heavy query to read model:

    sql
    // ❌ 40 JOINs on write schema — 800ms p95
    SELECT 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 events
    SELECT * FROM order_dashboard WHERE customer_id = ?
    // Write side unchanged — normalized aggregate integrity

    Execution workflow

    1CQRS adoption workflow
    1 / 4

    Measure read pain

    Slow queries, lock contention — profile first.

    800ms dashboard story.

    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.

    3 questions
    1IntermediateQuestionCQRS vs CRUD same model?+

    Answer

    CRUD: one schema read/write. CQRS: separate optimized models — write normalized, read denormalized, synced via events/projections.

    Follow-up

    800ms story?
    2AdvancedQuestionEventual consistency UX?+

    Answer

    Read lags after write — show loading state, poll, or optimistic UI. Accept or use read-after-write routing for critical paths.

    Follow-up

    Strong consistency when?
    3IntermediateQuestionCQRS without Event Sourcing?+

    Answer

    Yes — project from domain events or change data capture from write DB. ES is one way to feed projections.

    Follow-up

    Materialized view?

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.