CQRS
Command Query Responsibility Segregation (CQRS) separates write models (commands, transactional consistency) from read models (queries, denormalized projections).
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:
POST /commands/profile/update → ProfileCommandService↓PostgreSQL (normalized)↓ outboxKafka: profile.events↓┌─────────────────────────┼─────────────────────────┐↓ ↓ ↓FeedProjectionWorker SearchIndexer AnalyticsSink↓ ↓Redis (feed slices) ElasticsearchGET /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.
Production code example
Command handler + projection consumer — LinkedIn-style separation:
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.
1AdvancedQuestionWhen does CQRS hurt more than it helps?+
Answer
Follow-up
2AdvancedQuestionCQRS vs caching on a single database — decision criteria?+
Answer
Follow-up
3AdvancedQuestionDesign CQRS for LinkedIn-style feed with 500M members.+
Answer
Follow-up
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.