Topic Overview

CQRS

Master CQRS (Command Query Responsibility Segregation): separate read and write models, when to use it, and implementation patterns.

CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates read and write operations into different models. Commands (writes) and queries (reads) use different data models optimized for their specific needs.


What is CQRS?

CQRS separates:

  • Commands (Writes): Modify state, use write model
  • Queries (Reads): Read data, use read model

Traditional (Single Model):

Read → Database → Write
Same model for both

CQRS (Separate Models):

Read → Read Model (optimized for queries)
Write → Write Model (optimized for writes)

Benefits:

  • Performance: Optimize each model independently
  • Scalability: Scale reads and writes separately
  • Flexibility: Different technologies for read/write
  • Complexity: Handle complex read requirements

When to Use CQRS

Good Use Cases

1. High Read/Write Ratio

Many reads, few writes
Optimize read model for queries

2. Complex Queries

Read model optimized for complex queries
Write model optimized for domain logic

3. Different Scaling Needs

Reads: Scale horizontally
Writes: Scale differently

4. Event Sourcing

Write: Events
Read: Projections (read model)

Not Good For

1. Simple CRUD

Overhead not worth it
Traditional model sufficient

2. Low Complexity

No complex queries
Single model simpler

CQRS Architecture

Basic CQRS

Commands → Write Model → Database
Queries → Read Model → Database (or cache)

CQRS with Event Sourcing

Commands → Write Model → Events → Event Store
Events → Projections → Read Model → Database
Queries → Read Model

Examples

Basic CQRS Implementation

// Write Model (Commands)
class OrderWriteModel {
  async createOrder(command: CreateOrderCommand): Promise<void> {
    // Domain logic
    const order = new Order(command.userId, command.items);
    order.validate();
    
    // Save to write database
    await this.writeDatabase.save(order);
    
    // Publish event
    await this.eventBus.publish('order.created', order);
  }
  
  async updateOrder(command: UpdateOrderCommand): Promise<void> {
    const order = await this.writeDatabase.get(command.orderId);
    order.update(command.data);
    await this.writeDatabase.save(order);
    await this.eventBus.publish('order.updated', order);
  }
}

// Read Model (Queries)
class OrderReadModel {
  async getOrder(orderId: string): Promise<OrderView> {
    // Optimized read query
    return await this.readDatabase.query(`
      SELECT 
        o.id,
        o.user_id,
        o.total,
        u.name as user_name,
        COUNT(oi.id) as item_count
      FROM orders o
      JOIN users u ON o.user_id = u.id
      LEFT JOIN order_items oi ON o.id = oi.order_id
      WHERE o.id = ?
      GROUP BY o.id
    `, [orderId]);
  }
  
  async getUserOrders(userId: string): Promise<OrderView[]> {
    // Optimized for list queries
    return await this.readDatabase.query(`
      SELECT * FROM order_views
      WHERE user_id = ?
      ORDER BY created_at DESC
    `, [userId]);
  }
}

// Projection (Updates Read Model)
class OrderProjection {
  constructor() {
    this.eventBus.subscribe('order.created', async (event) => {
      await this.updateReadModel(event);
    });
    
    this.eventBus.subscribe('order.updated', async (event) => {
      await this.updateReadModel(event);
    });
  }
  
  async updateReadModel(event: Event): Promise<void> {
    // Update read model from event
    const orderView = this.mapToView(event.data);
    await this.readDatabase.upsert('order_views', orderView);
  }
}

CQRS with Event Sourcing

// Write Model (Event Sourcing)
class OrderAggregate {
  private events: Event[] = [];
  
  async createOrder(command: CreateOrderCommand): Promise<void> {
    const event = new OrderCreatedEvent({
      orderId: this.generateId(),
      userId: command.userId,
      items: command.items,
      timestamp: Date.now()
    });
    
    this.apply(event);
    await this.eventStore.append(event);
  }
  
  apply(event: Event): void {
    this.events.push(event);
    // Update aggregate state
  }
  
  async reconstruct(orderId: string): Promise<Order> {
    const events = await this.eventStore.getEvents(orderId);
    let order = new Order();
    for (const event of events) {
      order.apply(event);
    }
    return order;
  }
}

// Read Model (Projections)
class OrderReadModel {
  async updateFromEvent(event: Event): Promise<void> {
    switch (event.type) {
      case 'order.created':
        await this.createOrderView(event);
        break;
      case 'order.updated':
        await this.updateOrderView(event);
        break;
    }
  }
  
  async createOrderView(event: OrderCreatedEvent): Promise<void> {
    const view = {
      id: event.orderId,
      userId: event.userId,
      itemCount: event.items.length,
      total: this.calculateTotal(event.items),
      createdAt: event.timestamp
    };
    
    await this.readDatabase.insert('order_views', view);
  }
}

Common Pitfalls

  • Over-engineering: Using CQRS for simple cases. Fix: Only use when benefits outweigh complexity
  • Eventual consistency: Read model may be stale. Fix: Accept eventual consistency, or use synchronous updates
  • Complexity: More moving parts. Fix: Start simple, add complexity gradually
  • Not handling failures: Projection failures not handled. Fix: Implement retries, error handling
  • Read model sync: Read model out of sync. Fix: Monitor lag, implement catch-up

Interview Questions

Beginner

Q: What is CQRS and why is it used?

A:

CQRS (Command Query Responsibility Segregation) separates read and write operations into different models.

Why used:

  • Performance: Optimize each model independently
  • Scalability: Scale reads and writes separately
  • Flexibility: Different technologies for read/write
  • Complexity: Handle complex read requirements

Traditional:

Read → Database → Write
Same model for both

CQRS:

Read → Read Model (optimized for queries)
Write → Write Model (optimized for writes)

Example:

Write Model: Normalized, domain logic
Read Model: Denormalized, optimized for queries

Benefits:

  • Read model optimized for complex queries
  • Write model optimized for domain logic
  • Scale independently
  • Use different technologies

Intermediate

Q: Explain how CQRS works with event sourcing. How do you keep read and write models in sync?

A:

CQRS with Event Sourcing:

Write Side:

Commands → Write Model → Events → Event Store

Read Side:

Events → Projections → Read Model
Queries → Read Model

How it works:

  1. Command: Create order
  2. Write Model: Validates, creates event
  3. Event Store: Stores event
  4. Projection: Reads event, updates read model
  5. Query: Reads from read model

Keeping in sync:

Event Store → Projection → Read Model

Projection subscribes to events
Updates read model asynchronously

Example:

// Write: Create order
await orderService.createOrder(data);
// Event: OrderCreated stored

// Projection: Updates read model
eventBus.subscribe('order.created', async (event) => {
  await readModel.update(event);
});

// Read: Query read model
const orders = await readModel.getUserOrders(userId);

Eventual Consistency:

  • Read model may be slightly stale
  • Acceptable for most use cases
  • Can use synchronous updates if needed

Senior

Q: Design a CQRS system for a social media platform with millions of users. How do you handle read/write separation, event projections, and ensure consistency?

A:

class SocialMediaCQRS {
  private writeModel: WriteModel;
  private readModel: ReadModel;
  private eventStore: EventStore;
  private projections: Projection[];
  
  constructor() {
    this.writeModel = new WriteModel();
    this.readModel = new ReadModel();
    this.eventStore = new EventStore();
    this.projections = [
      new UserProjection(),
      new PostProjection(),
      new FeedProjection()
    ];
  }
  
  // 1. Write Model (Commands)
  class WriteModel {
    async createPost(command: CreatePostCommand): Promise<void> {
      // Domain logic
      const post = new Post(command.userId, command.content);
      post.validate();
      
      // Create event
      const event = new PostCreatedEvent({
        postId: post.id,
        userId: post.userId,
        content: post.content,
        timestamp: Date.now()
      });
      
      // Store event
      await this.eventStore.append(event);
    }
    
    async likePost(command: LikePostCommand): Promise<void> {
      const event = new PostLikedEvent({
        postId: command.postId,
        userId: command.userId,
        timestamp: Date.now()
      });
      
      await this.eventStore.append(event);
    }
  }
  
  // 2. Read Model (Queries)
  class ReadModel {
    async getUserFeed(userId: string, page: number): Promise<PostView[]> {
      // Optimized query (denormalized)
      return await this.readDatabase.query(`
        SELECT 
          p.id,
          p.content,
          u.name as author_name,
          u.avatar,
          p.like_count,
          p.comment_count,
          p.created_at
        FROM post_views p
        JOIN user_views u ON p.user_id = u.id
        WHERE p.user_id IN (
          SELECT followee_id FROM follows WHERE follower_id = ?
        )
        ORDER BY p.created_at DESC
        LIMIT 20 OFFSET ?
      `, [userId, page * 20]);
    }
    
    async getPostDetails(postId: string): Promise<PostDetailView> {
      // Optimized for single post
      return await this.readDatabase.get('post_detail_views', postId);
    }
  }
  
  // 3. Projections
  class PostProjection {
    constructor() {
      this.eventStore.subscribe('post.created', async (event) => {
        await this.handlePostCreated(event);
      });
      
      this.eventStore.subscribe('post.liked', async (event) => {
        await this.handlePostLiked(event);
      });
    }
    
    async handlePostCreated(event: PostCreatedEvent): Promise<void> {
      // Update read model
      const view = {
        id: event.postId,
        userId: event.userId,
        content: event.content,
        likeCount: 0,
        commentCount: 0,
        createdAt: event.timestamp
      };
      
      await this.readDatabase.insert('post_views', view);
    }
    
    async handlePostLiked(event: PostLikedEvent): Promise<void> {
      // Update like count
      await this.readDatabase.increment('post_views', 'like_count', {
        id: event.postId
      });
    }
  }
  
  // 4. Event Store
  class EventStore {
    async append(event: Event): Promise<void> {
      await this.database.insert('events', {
        id: event.id,
        type: event.type,
        aggregateId: event.aggregateId,
        data: event.data,
        timestamp: event.timestamp,
        version: event.version
      });
      
      // Publish to projections
      await this.publish(event);
    }
    
    async getEvents(aggregateId: string): Promise<Event[]> {
      return await this.database.query(
        'SELECT * FROM events WHERE aggregate_id = ? ORDER BY version',
        [aggregateId]
      );
    }
  }
  
  // 5. Consistency
  class ConsistencyManager {
    async checkLag(): Promise<number> {
      // Check projection lag
      const lastEvent = await this.eventStore.getLastEvent();
      const lastProcessed = await this.getLastProcessedEvent();
      
      return lastEvent.version - lastProcessed.version;
    }
    
    async catchUp(): Promise<void> {
      // Catch up projections
      const lag = await this.checkLag();
      if (lag > 100) {
        await this.replayEvents();
      }
    }
  }
}

Features:

  1. Write model: Domain logic, event sourcing
  2. Read model: Optimized queries, denormalized
  3. Projections: Update read model from events
  4. Event store: Persistent event storage
  5. Consistency: Monitor lag, catch up if needed

Key Takeaways

  • CQRS: Separates read and write operations into different models
  • Commands: Write operations, use write model
  • Queries: Read operations, use read model
  • Benefits: Performance, scalability, flexibility
  • Event sourcing: Write model produces events, projections update read model
  • Eventual consistency: Read model may be slightly stale
  • When to use: High read/write ratio, complex queries, different scaling needs
  • Best practices: Accept eventual consistency, monitor projection lag, handle failures

About the author

InterviewCrafted helps you master system design with patience. We believe in curiosity-led engineering, reflective writing, and designing systems that make future changes feel calm.