Day 022: CQRS - Separating Reads from Writes (CQRS pattern fundamentals)

Day 022: CQRS - Separating Reads from Writes

Topic: CQRS pattern fundamentals

πŸ’‘ Today's "Aha!" Moment

The insight: Don't use the same model for writes and reads. Commands change state, queries read stateβ€”they have different needs. CQRS separates them to optimize each independently.

Why this matters:

Traditional architecture uses one model for everything:

User β†’ API β†’ Single Model β†’ Database
         ↓
    (Handles both writes AND reads)

This creates conflicts:

Same model can't optimize for both. Write-optimized schema (normalized, third normal form) makes reads slow (JOINs everywhere). Read-optimized schema (denormalized) makes writes complex (update in multiple places).

CQRS splits the model:

Commands (Write Side):
User β†’ CommandHandler β†’ Write Model β†’ Event Store
                              ↓
                         (Publishes events)
                              ↓
Queries (Read Side):      Event Bus
User β†’ QueryHandler ← Read Model ← (Subscribes to events)

Write side: Focus on business logic, validation, consistency

🌟 Why This Matters

CQRS enables independent scaling and optimization of read and write workloads. Companies like Amazon, Netflix, and LinkedIn use this pattern to handle millions of operations while maintaining flexibility and performance.

🎯 Daily Objective

Master Command Query Responsibility Segregation (CQRS): understand how separating write models from read models enables scalability, flexibility, and optimized performance. Build a CQRS system that demonstrates the pattern's power.

πŸ“š Topics Covered

Read side: Focus on speed, denormalization, specific views
Sync via events: Changes flow from write β†’ read asynchronously

The pattern: Separate models for commands (writes) and queries (reads)

This pattern appears wherever read and write needs differ:

How to recognize CQRS fits:

Use CQRS when: Skip CQRS when:
Reads far exceed writes (100:1+) Equal read/write ratio
Need multiple read views from same data Single simple view suffices
Complex business logic on writes Simple CRUD operations
Read performance critical (search, dashboards) Performance not critical
Event-driven architecture already in place Monolithic synchronous system

Common misconceptions:

❌ Myth: "CQRS requires Event Sourcing"
βœ… Truth: CQRS and Event Sourcing are independent. Can use CQRS with traditional database (write model updates DB, triggers sync to read model).

❌ Myth: "CQRS means eventual consistency always"
βœ… Truth: Can be synchronous (write model updates read model in same transaction). Usually async for scale, but not required.

❌ Myth: "CQRS is all-or-nothing"
βœ… Truth: Apply selectivelyβ€”only for bounded contexts where it adds value. Rest of system can be traditional.

❌ Myth: "Two databases = CQRS"
βœ… Truth: CQRS is about two models, not two databases. Can have both models in same DB (different schemas).

Real-world examples:

  1. Netflix:

  2. Write side: User rates movie, updates profile β†’ Cassandra (consistent writes)

  3. Read side: Homepage recommendations β†’ ElasticSearch (fast queries)
  4. Events: RatingChanged, ProfileUpdated β†’ sync read models

  5. Amazon product catalog:

  6. Write side: Seller updates product β†’ RDBMS (transactions, validation)

  7. Read side:
    • Search β†’ ElasticSearch (full-text, facets)
    • Recommendations β†’ Graph DB (related products)
    • Mobile app β†’ NoSQL (fast denormalized reads)
  8. Same data, multiple read models optimized for different queries

  9. Uber:

  10. Write side: Trip requested β†’ Write DB (availability, pricing, matching)

  11. Read side:

    • Rider app β†’ "Where's my driver?" (location updates)
    • Driver app β†’ "New trip request" (geospatial queries)
    • Analytics β†’ Data warehouse (reporting)
  12. LinkedIn feed:

  13. Write side: User posts update β†’ Master DB (consistency)

  14. Read side: Feed queries β†’ Cached, denormalized views (billions of reads/day)
  15. Events: PostCreated β†’ rebuild affected feeds asynchronously

  16. GitHub pull requests:

  17. Write side: Comment, approve, merge β†’ Git + metadata DB
  18. Read side: PR timeline view β†’ Denormalized (all comments, reviews, commits together)

What changes after this realization:

You stop forcing one model to serve two masters. Instead of fighting between "normalize for writes" vs "denormalize for reads," do both in separate models.

You recognize eventual consistency is OK for reads:

You understand query models are projections:

// Write Model (normalized)
tables: users, posts, comments, likes

// Read Model (denormalized for feed query)
{
  postId: "123",
  author: { name, avatar },
  content: "...",
  commentCount: 42,
  likeCount: 156,
  topComments: [...]  // Pre-computed!
}

Read model optimized for specific query (no JOINs, no counting, instant response).

You learn synchronization strategies:

  1. Event-driven (most common): Write side publishes events, read side subscribes
  2. Change Data Capture (CDC): Database triggers/log tailing updates read models
  3. Dual writes: Update both models (risky, can get out of sync)
  4. Scheduled sync: Batch updates (acceptable for analytics)

Trade-offs:

Meta-insight:

CQRS acknowledges that reading and writing are fundamentally different operations. Trying to use one model for both is like using a hammer for screwsβ€”it works but it's not optimal.

This mirrors real-world specialization:

The deeper principle: Optimize for actual usage patterns. If 99% of traffic is reads, don't let write constraints slow reads. If writes need consistency, don't let read denormalization complicate writes.

CQRS is permission to specializeβ€”build the best write model for writes, best read model for reads, and sync them. Complexity increases, but each piece becomes simpler because it has one job.

The most elegant systems aren't the cleverestβ€”they're the ones that match structure to use case. CQRS does this by acknowledging that commands and queries are different beasts deserving different treatment.

πŸ“– Detailed Curriculum (Optimized for 60 min)

  1. Video Introduction (12 min) ⭐ START HERE

  2. Udi Dahan: "CQRS - Clarified"

  3. Video Link
  4. Pattern explanation and common mistakes

  5. Core Reading (18 min)

  6. Martin Fowler: "CQRS"

  7. Article
  8. Microsoft CQRS Journey: Chapter 1
  9. Focus: Core pattern, when to use, scalability benefits

  10. Quick Synthesis (5 min)

  11. Draw: Command side vs Query side diagram
  12. Write: "Why separate reads and writes?"
  13. Note: 3 benefits and 3 challenges

πŸ“‘ Resources

🎯 Core Resources (Use Today)

⭐ Bonus Resources (If Extra Time)

✍️ Practical Activities (25 min total)

1. Implement CQRS System (15 min)

Build a simple blog system with CQRS:

// COMMAND SIDE (Write Model)
// Handles business logic and state changes

class PostCommands {
    eventStore

    createPost(postId, title, content, authorId) {
        // Validate
        if (title.isEmpty()) throw "Title required"

        // Create event
        event = PostCreated {
            postId, title, content, authorId,
            timestamp: now()
        }

        // Store event
        eventStore.append(event)

        // Publish event to read side
        eventBus.publish(event)
    }

    publishPost(postId) {
        // Load post from events
        post = Post.fromEvents(eventStore.getEvents(postId))

        // Validate can publish
        if (post.isPublished) throw "Already published"

        // Create and store event
        event = PostPublished { postId, timestamp: now() }
        eventStore.append(event)
        eventBus.publish(event)
    }
}

// QUERY SIDE (Read Model)
// Optimized for fast reads

class PostQueries {
    database // Denormalized read database

    // Listen to events and update read models
    onEvent(event) {
        match event {
            PostCreated => {
                database.insert({
                    id: event.postId,
                    title: event.title,
                    content: event.content,
                    author: event.authorId,
                    status: "draft",
                    createdAt: event.timestamp
                })
            }
            PostPublished => {
                database.update(event.postId, {
                    status: "published",
                    publishedAt: event.timestamp
                })
            }
        }
    }

    // Optimized query methods
    getPost(postId) {
        return database.find(postId)
    }

    getRecentPosts(limit) {
        return database.query(
            where: { status: "published" },
            orderBy: "publishedAt DESC",
            limit: limit
        )
    }

    getPostsByAuthor(authorId) {
        return database.query(
            where: { author: authorId },
            orderBy: "createdAt DESC"
        )
    }
}

// EVENT BUS (Communication between sides)
class EventBus {
    subscribers = []

    subscribe(handler) {
        subscribers.push(handler)
    }

    publish(event) {
        for handler in subscribers {
            handler.handle(event)
        }
    }
}

Tasks:

2. Architecture Diagram (7 min)

Draw the complete CQRS architecture:

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   Client    β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
               β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
        β”‚             β”‚
    Commands      Queries
        β”‚             β”‚
        v             v
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Command  β”‚   β”‚  Query   β”‚
β”‚   Side    β”‚   β”‚   Side   β”‚
β”‚  (Write)  β”‚   β”‚  (Read)  β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”˜
      β”‚               β”‚
      v               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
β”‚  Event   │───▢│  Event   β”‚
β”‚  Store   β”‚    β”‚  Handler β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                      v
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚   Read   β”‚
                β”‚ Database β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. Reflection (3 min)

Answer:

🎨 Creativity - Quick Mental Reset (5 min)

Quick Exercise: "Command vs Query"

Draw two faces:

Label them with examples:

Purpose: Internalize the mental model of commands changing state vs queries reading state.

Take 3 min to draw, 2 min to think about how they differ in your own systems

πŸ”— Connections to Previous Learning

From Yesterday:

From Month 1:

Building Forward:

βœ… Daily Deliverables (Must Complete)

⭐ Bonus (If Extra Time)

🎯 Success Criteria

By the end of today, you should be able to:

  1. βœ… Explain CQRS pattern and its motivation
  2. βœ… Implement separate command and query models
  3. βœ… Connect write and read sides via events
  4. βœ… Describe eventual consistency trade-offs
  5. βœ… Identify when CQRS is appropriate
  6. βœ… Design denormalized read models

⏰ Total Estimated Time (OPTIMIZED)

Note: Focus on the pattern, not perfection. A simple working example that shows separate read/write models is excellent!


πŸ“ Today's Big Ideas

  1. Separation of Concerns: Writes optimize for consistency, reads for performance
  2. Eventual Consistency: Read models update asynchronously
  3. Scalability: Read and write sides scale independently
  4. Flexibility: Multiple read models from same write model
  5. Simplicity: Each side is simpler because it has one job

πŸ’‘ CQRS in the Real World


πŸš€ Tomorrow's Preview

Saga Pattern: Distributed Transactions

You'll learn how to coordinate long-running transactions across multiple services without distributed transactions. Sagas use commands and events to maintain consistency in microservices!


"CQRS is not a top-level architecture. It's a pattern that's applicable in specific places." - Martin Fowler

You're mastering production patterns! πŸ’ͺ



← Back to Learning