Jonatan Matajonmatum.com
conceptsnotesexperimentsessays
© 2026 Jonatan Mata. All rights reserved.v2.1.1
Concepts

CQRS

Pattern separating read and write operations into distinct models, optimizing each independently for performance and scalability.

evergreen#cqrs#architecture#patterns#event-sourcing#read-write#scaling

What it is

CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates write operations (commands) from read operations (queries) into completely distinct models. Unlike traditional CRUD systems where the same model handles both operations, CQRS allows each side to use its own database, schema, technology, and scaling strategy.

The pattern builds on Bertrand Meyer's CQS (Command Query Separation) principle but extends it to the architectural level. Commands modify system state without returning data, while queries return data without modifying state. This separation enables specific optimizations for each type of operation.

CQRS doesn't necessarily require event sourcing, although both patterns complement each other naturally. It can be implemented with traditional relational databases, using materialized views or synchronization processes to keep read models updated.

Practical implementation

Example with Node.js and PostgreSQL

// Command side - Write model
interface CreateOrderCommand {
  customerId: string;
  items: OrderItem[];
  shippingAddress: Address;
}
 
class OrderCommandHandler {
  constructor(
    private orderRepository: OrderRepository,
    private eventBus: EventBus
  ) {}
 
  async handle(command: CreateOrderCommand): Promise<void> {
    const order = new Order(
      command.customerId,
      command.items,
      command.shippingAddress
    );
    
    await this.orderRepository.save(order);
    
    // Publish event for read model projection
    await this.eventBus.publish(new OrderCreatedEvent(order));
  }
}
 
// Query side - Read model
interface OrderSummaryQuery {
  customerId: string;
  status?: OrderStatus;
  dateRange?: DateRange;
}
 
class OrderQueryHandler {
  constructor(private readDatabase: ReadDatabase) {}
 
  async handle(query: OrderSummaryQuery): Promise<OrderSummary[]> {
    return this.readDatabase.query(`
      SELECT 
        order_id,
        customer_name,
        total_amount,
        status,
        created_at
      FROM order_summaries 
      WHERE customer_id = $1
        AND ($2::text IS NULL OR status = $2)
        AND created_at BETWEEN $3 AND $4
      ORDER BY created_at DESC
    `, [query.customerId, query.status, query.dateRange?.start, query.dateRange?.end]);
  }
}
 
// Event handler for projection
class OrderProjectionHandler {
  async on(event: OrderCreatedEvent): Promise<void> {
    await this.readDatabase.execute(`
      INSERT INTO order_summaries (
        order_id, customer_id, customer_name, 
        total_amount, status, created_at
      ) VALUES ($1, $2, $3, $4, $5, $6)
    `, [
      event.orderId,
      event.customerId,
      event.customerName,
      event.totalAmount,
      'PENDING',
      event.timestamp
    ]);
  }
}

Projection patterns

Immediate vs eventual projection

Loading diagram...

Immediate projection: Updates the read model in the same transaction. Guarantees consistency but reduces performance and increases coupling.

Eventual projection: Uses asynchronous events to update the read model. Higher performance and decoupling, but requires handling eventual consistency.

Handling eventual consistency

class OrderQueryService {
  async getOrderWithFallback(orderId: string): Promise<OrderView> {
    // Try read model first
    const orderView = await this.readModel.findById(orderId);
    
    if (orderView) {
      return orderView;
    }
    
    // Fallback to write model if not yet projected
    const order = await this.writeModel.findById(orderId);
    if (!order) {
      throw new OrderNotFoundError(orderId);
    }
    
    // Build view on-the-fly
    return this.buildOrderView(order);
  }
  
  private buildOrderView(order: Order): OrderView {
    return {
      id: order.id,
      customerName: order.customer.name,
      status: order.status,
      totalAmount: order.calculateTotal(),
      createdAt: order.createdAt
    };
  }
}

CQRS without Event Sourcing

CQRS can be implemented without event sourcing using traditional data synchronization:

class OrderService {
  async createOrder(command: CreateOrderCommand): Promise<void> {
    const transaction = await this.database.beginTransaction();
    
    try {
      // Update write model
      const order = await this.orderRepository.create(command, transaction);
      
      // Update read model in same transaction
      await this.orderSummaryRepository.create({
        orderId: order.id,
        customerId: order.customerId,
        totalAmount: order.totalAmount,
        status: order.status
      }, transaction);
      
      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }
}

Decision framework

CriteriaCQRS RecommendedTraditional CRUD
Read/write ratio> 10:1< 5:1
Query complexityHigh, multiple viewsSimple, basic CRUD
Scaling requirementsIndependent per operationUniform scaling
ConsistencyEventual acceptableImmediate required
Team experienceDistributed systemsMonolithic applications
Business domainComplex, multiple contextsSimple, single context

Common anti-patterns

Over-engineering: Applying CQRS to simple CRUD systems adds unnecessary complexity. Not every bounded context needs CQRS.

Synchronous projections: Updating read models synchronously eliminates scalability benefits and can create bottlenecks.

Normalized read models: Maintaining normalization in read models wastes the opportunity to optimize for specific queries.

Lack of versioning: Not versioning events or projection schemas complicates system evolution and migrations.

Why it matters

CQRS enables independent optimization of reads and writes, which is critical in systems where access patterns differ significantly. In microservices architectures, it enables each service to optimize its data model for its specific domain. The separation facilitates horizontal scaling: read models can be replicated geographically while write models remain centralized. Combined with event-driven architecture, CQRS enables building resilient systems that can rebuild state from events, facilitating debugging, auditing, and historical analysis.

References

  • CQRS — Martin Fowler. Canonical definition of the pattern and when to apply it.
  • CQRS Pattern - Azure Architecture Center | Microsoft Learn — Microsoft, 2024. Implementation guide with practical examples.
  • A Beginner's Guide to CQRS — Event Store, 2024. CQRS with event sourcing and projection patterns.
  • CQRS pattern - AWS Prescriptive Guidance — AWS, 2024. Implementation in cloud-native architectures.
  • Pattern: Command Query Responsibility Segregation (CQRS) — Chris Richardson. CQRS in the context of microservices.
  • Event sourcing, CQRS, stream processing and Apache Kafka: What's the connection? | Confluent — Confluent, 2023. Integration with streaming systems.

Related content

  • Event-Driven Architecture

    Architectural pattern where components communicate through asynchronous events, enabling decoupled, scalable, and reactive systems.

  • Domain-Driven Design

    Software design approach centering development on the business domain, using a ubiquitous language shared between developers and domain experts.

  • Event Sourcing

    Pattern where application state is derived from an immutable sequence of events, providing complete audit trail and the ability to reconstruct state at any point in time.

  • Saga Pattern

    Pattern for managing distributed transactions in microservices through a sequence of local transactions with compensating actions to handle failures.

Concepts