Enterprise Architecture Patterns Tutorial 0/65 lessons ~6 min read Lesson 33

    CQRS

    Command Query Responsibility Segregation (CQRS) separates write models (commands, transactional consistency) from read models (queries, denormalized projections).

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

    Introduction

    Command Query Responsibility Segregation (CQRS) separates write models (commands, transactional consistency) from read models (queries, denormalized projections). LinkedIn's activity feed and profile views use distinct stores optimized for each access pattern — writes go through authoritative services; reads hit materialized views rebuilt from events.

    Real production story

    LinkedIn's unified profile API served both member edits and homepage feed reads from one Cassandra cluster. Profile updates triggered expensive fan-out reads; feed p99 latency spiked whenever HR teams ran bulk headcount imports. DBAs could not tune one schema for OLTP writes and OLAP-style timeline scans.

    The architecture team split commands (MemberProfileService) from queries (FeedViewService backed by Kafka Streams projections). Write path validates invariants and emits MemberProfileUpdated; read path serves precomputed feed slices from Redis + Espresso. Feed p99 dropped 40%; write throughput scaled independently on a smaller, normalized store.

    Business problem

    Business pressure: LinkedIn must serve billions of feed impressions daily while members edit profiles in real time. One model forcing both shapes creates impossible indexing and caching trade-offs.

    • User experience: Feed scroll latency directly affects session time and ad revenue.
    • Write integrity: Profile and connection graph mutations require strong validation — not eventual guesswork on read path.
    • Team scale: Feed ranking and profile storage evolve on different release trains.

    Architecture overview

    Commands mutate state through aggregates and emit events. Queries never touch write DB — they read projections that may lag milliseconds to seconds behind.

    • Definition: Separate models, APIs, and often databases for reads vs writes.
    • When to adopt: Complex domains with divergent read shapes (feeds, dashboards, search).
    • When to defer: CRUD apps with symmetric read/write — added complexity without payoff.
    • Operability: Track projection lag; rebuild tooling for corrupted read models.

    Architecture motivation

    Why architects care: CQRS makes performance and consistency explicit per path — commands optimize for correctness; queries optimize for shape and cache locality.

    • Force: Read/write ratio exceeds 100:1 on social surfaces; different SLAs per path.
    • Constraint: Cannot duplicate business rules in projection code without drift.
    • Outcome: Single write model; multiple read models rebuilt from event log or outbox.

    Internal architecture

    LinkedIn-style CQRS — command side authoritative; query side eventually consistent:

    text
    POST /commands/profile/update → ProfileCommandService
    PostgreSQL (normalized)
    ↓ outbox
    Kafka: profile.events
    ┌─────────────────────────┼─────────────────────────┐
    ↓ ↓ ↓
    FeedProjectionWorker SearchIndexer AnalyticsSink
    ↓ ↓
    Redis (feed slices) Elasticsearch
    GET /views/feed/{memberId} → FeedQueryService → Redis only (never write DB)

    Data flow

    Commands are synchronous-strong; queries are asynchronous-optimized.

    • Write path: Command → aggregate validation → persist → domain event.
    • Read path: Query DTO from projection store; stale-read policy documented (max lag 2s).
    • Rebuild path: Replay events from offset 0 to reconstruct projection after schema change.

    System design diagram

    Two diagrams show the CQRS topology and the primary request/event path used in production at scale.

    CQRS — system view
    Command API
    Edge
    Write store
    Core
    Event bus
    Data
    Read projections
    Async
    High-level topology for CQRS.
    CQRS — request / event flow
    Command
    Ingress
    Validate + persist
    Store
    Emit event
    Store
    Update read model
    Emit
    Follow this path when reviewing production designs.

    Production code example

    Command handler + projection consumer — LinkedIn-style separation:

    typescript
    class UpdateProfileCommandHandler {
    async handle(cmd: UpdateProfileCommand): Promise<void> {
    await this.db.transaction(async (tx) => {
    const profile = await tx.profiles.findForUpdate(cmd.memberId);
    profile.apply(cmd.changes);
    await tx.profiles.save(profile);
    await tx.outbox.insert({
    type: "MemberProfileUpdated",
    aggregateId: cmd.memberId,
    payload: profile.toEventPayload(),
    });
    });
    }
    }
    class FeedProjectionConsumer {
    async onMemberProfileUpdated(evt: MemberProfileUpdated): Promise<void> {
    const connections = await this.graph.followers(evt.memberId);
    const slice = FeedSlice.fromProfileChange(evt);
    for (const followerId of connections) {
    await this.redis.zadd(
    `feed:${followerId}`,
    evt.timestamp,
    JSON.stringify(slice),
    );
    }
    this.metrics.projectionLag.observe(Date.now() - evt.timestamp);
    }
    }

    Enterprise case study

    LinkedIn activity feed CQRS migration — separated profile writes from feed reads.

    • Before: Shared Cassandra; feed p99 420ms during bulk imports.
    • Decision: Kafka-backed feed projections + Redis serving layer; commands stay on normalized OLTP.
    • After: Feed p99 180ms; write path unaffected by feed traffic spikes.

    Trade-offs

    • Complexity vs performance: Two pipelines to deploy, monitor, and debug — buys independent scaling.
    • Consistency vs latency: Read-your-writes may require routing recent commands to read model or client-side merge.
    • Duplication risk: Query models duplicate data — storage cost and GDPR deletion complexity increase.
    • Eventual lag: Projector bugs cause silent read corruption until detected by reconciliation.

    Security considerations

    Read models often over-fetch for performance — authorization must filter projections, not just APIs.

    • Field-level ACL: Projections store only what query role may see; no "filter in API" shortcut.
    • PII fan-out: Each projection copy expands GDPR erasure scope — track downstream stores.
    • Command authZ: Commands validated against member identity before any event emission.

    Scalability analysis

    CQRS shines when read tier scales 10× beyond write tier — LinkedIn feed readers dwarf profile editors.

    • Horizontal scale: Stateless query APIs front Redis/Espresso clusters; command service scales on write QPS.
    • Hot members: Celebrity profiles need dedicated projection shards or CDN edge caching.
    • Cost: Measure $/1M feed impressions vs $/1K profile updates — justify projection count.

    Failure scenarios

    Projection failure is a read outage, not a write outage — design degraded read modes.

    • Projector stuck: Lag alert fires; serve stale feed with banner or fall back to simplified timeline.
    • Schema migration: Blue/green projections — dual-write events until new consumer caught up.
    • Split brain: Two command handlers if misconfigured — enforce single writer per aggregate ID.

    Staff engineer insights

    • CQRS without event sourcing is valid — outbox + projections still work; do not over-buy event store complexity.
    • If projection lag is not on the exec dashboard, you will ship features that assume strong read-after-write falsely.
    • One command model, many query models — resist letting each mobile client invent its own write path.

    Interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1AdvancedQuestionWhen does CQRS hurt more than it helps?+

    Answer

    Simple CRUD with symmetric reads, small team, low read/write ratio — you pay operational tax for two pipelines without performance win. Also avoid CQRS as org chart politics ("my service owns reads").

    Follow-up

    How do you implement read-your-writes without hitting write DB?
    2AdvancedQuestionCQRS vs caching on a single database — decision criteria?+

    Answer

    Cache helps uniform reads; CQRS helps when read shapes differ radically (feed vs profile vs search) or read tier must scale independently. If one normalized schema + Redis covers SLOs, skip CQRS.

    Follow-up

    How do you rebuild a corrupted projection?
    3AdvancedQuestionDesign CQRS for LinkedIn-style feed with 500M members.+

    Answer

    Command: normalized write DB + outbox. Projections: partition Kafka by memberId; fan-out on write to follower feeds sharded in Redis; query API never touches write store; monitor lag per projection type.

    Follow-up

    Celebrity fan-out hot spot mitigation?

    Architecture review questions

    • Single authoritative write model — no duplicate command handlers per aggregate.
    • Projection lag SLO defined with alert and degraded-read runbook.
    • Replay/rebuild tooling tested on staging with full event volume sample.
    • Read models enforce same authZ rules as commands — field-level review.
    • GDPR erasure propagates to all projection stores — inventory documented.
    • ADR explains why cache-on-single-DB was insufficient.

    Summary

    CQRS at LinkedIn scale separates transactional writes from denormalized reads, letting feeds and profiles evolve independently. Success requires event-driven sync, lag monitoring, and rebuild tooling — not just two databases.

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