StackPractices
advanced Por Mathias Paulenko

Patrón CQRS

Separa las operaciones de lectura y escritura en modelos diferentes, optimizando cada uno para su carga de trabajo específica. Un patrón de datos para sistemas escalables.

Temas: design

Resumen

CQRS (Command Query Responsibility Segregation) es un patrón arquitectónico que separa las operaciones de lectura y escritura en modelos distintos. En lugar de usar un único modelo de datos para consultas y actualizaciones, CQRS usa un Modelo de Comandos para escrituras y un Modelo de Consultas para lecturas, cada uno optimizado para su carga de trabajo específica.

Cuándo usarlo

Usa el Patrón CQRS cuando:

  • Las cargas de trabajo de lectura y escritura tengan características de rendimiento muy diferentes o escalen independientemente
  • Necesites capacidades de consulta complejas (agregación, filtrado, búsqueda) sin complicar el modelo de escritura
  • El event sourcing ya esté en uso, haciendo que las proyecciones del modelo de lectura sean naturales
  • Diferentes equipos sean dueños de lecturas vs. escrituras, y el desacoplamiento reduzca coordinación
  • Ejemplos: dashboards de analytics, catálogos de e-commerce, feeds de redes sociales, sistemas de reportes

Solución

Python

from dataclasses import dataclass, field
from typing import List, Dict
from datetime import datetime

# Modelo de Escritura (Command Side)
@dataclass
class Order:
    order_id: str
    customer_id: str
    items: List[dict] = field(default_factory=list)
    status: str = "pending"
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())

class OrderCommandHandler:
    def __init__(self):
        self.orders: Dict[str, Order] = {}

    def create_order(self, order_id: str, customer_id: str) -> Order:
        order = Order(order_id=order_id, customer_id=customer_id)
        self.orders[order_id] = order
        return order

    def add_item(self, order_id: str, product: str, qty: int, price: float):
        order = self.orders[order_id]
        order.items.append({"product": product, "qty": qty, "price": price})

    def confirm(self, order_id: str):
        self.orders[order_id].status = "confirmed"

# Modelo de Lectura (Query Side) — optimizado para lecturas
@dataclass
class OrderSummary:
    order_id: str
    customer_id: str
    total: float
    item_count: int
    status: str

class OrderQueryHandler:
    def __init__(self, command_store: Dict[str, Order]):
        self.command_store = command_store

    def get_summary(self, order_id: str) -> OrderSummary:
        order = self.command_store[order_id]
        total = sum(i["qty"] * i["price"] for i in order.items)
        return OrderSummary(
            order_id=order.order_id,
            customer_id=order.customer_id,
            total=total,
            item_count=len(order.items),
            status=order.status
        )

    def list_by_customer(self, customer_id: str) -> List[OrderSummary]:
        return [
            self.get_summary(o.order_id)
            for o in self.command_store.values()
            if o.customer_id == customer_id
        ]

# Uso
commands = OrderCommandHandler()
commands.create_order("ORD-1", "CUST-1")
commands.add_item("ORD-1", "Laptop", 1, 999.99)
commands.add_item("ORD-1", "Mouse", 2, 29.99)
commands.confirm("ORD-1")

queries = OrderQueryHandler(commands.orders)
summary = queries.get_summary("ORD-1")
print(f"Order {summary.order_id}: ${summary.total:.2f} ({summary.item_count} items)")

JavaScript

// Modelo de Escritura (Command Side)
class OrderCommandHandler {
  constructor() {
    this.orders = new Map();
  }

  createOrder(orderId, customerId) {
    this.orders.set(orderId, {
      orderId, customerId, items: [], status: "pending",
      createdAt: new Date().toISOString()
    });
  }

  addItem(orderId, product, qty, price) {
    this.orders.get(orderId).items.push({ product, qty, price });
  }

  confirm(orderId) {
    this.orders.get(orderId).status = "confirmed";
  }
}

// Modelo de Lectura (Query Side)
class OrderQueryHandler {
  constructor(commandStore) {
    this.store = commandStore;
  }

  getSummary(orderId) {
    const order = this.store.get(orderId);
    const total = order.items.reduce((sum, i) => sum + i.qty * i.price, 0);
    return {
      orderId: order.orderId,
      customerId: order.customerId,
      total,
      itemCount: order.items.length,
      status: order.status
    };
  }

  listByCustomer(customerId) {
    return Array.from(this.store.values())
      .filter(o => o.customerId === customerId)
      .map(o => this.getSummary(o.orderId));
  }
}

// Uso
const commands = new OrderCommandHandler();
commands.createOrder("ORD-1", "CUST-1");
commands.addItem("ORD-1", "Laptop", 1, 999.99);
commands.confirm("ORD-1");

const queries = new OrderQueryHandler(commands.orders);
console.log(queries.getSummary("ORD-1"));

Java

import java.util.*;

// Modelo de Escritura
class Order {
    String orderId;
    String customerId;
    List<Map<String, Object>> items = new ArrayList<>();
    String status = "pending";
    String createdAt = new Date().toString();
}

class OrderCommandHandler {
    private final Map<String, Order> orders = new HashMap<>();

    public void createOrder(String orderId, String customerId) {
        Order o = new Order();
        o.orderId = orderId;
        o.customerId = customerId;
        orders.put(orderId, o);
    }

    public void addItem(String orderId, String product, int qty, double price) {
        Map<String, Object> item = new HashMap<>();
        item.put("product", product);
        item.put("qty", qty);
        item.put("price", price);
        orders.get(orderId).items.add(item);
    }

    public void confirm(String orderId) {
        orders.get(orderId).status = "confirmed";
    }

    public Map<String, Order> getStore() { return orders; }
}

// Modelo de Lectura
class OrderSummary {
    public String orderId;
    public String customerId;
    public double total;
    public int itemCount;
    public String status;
}

class OrderQueryHandler {
    private final Map<String, Order> store;

    public OrderQueryHandler(Map<String, Order> store) {
        this.store = store;
    }

    public OrderSummary getSummary(String orderId) {
        Order o = store.get(orderId);
        OrderSummary s = new OrderSummary();
        s.orderId = o.orderId;
        s.customerId = o.customerId;
        s.total = o.items.stream().mapToDouble(i ->
            (int)i.get("qty") * (double)i.get("price")).sum();
        s.itemCount = o.items.size();
        s.status = o.status;
        return s;
    }
}

// Uso
OrderCommandHandler commands = new OrderCommandHandler();
commands.createOrder("ORD-1", "CUST-1");
commands.addItem("ORD-1", "Laptop", 1, 999.99);
commands.confirm("ORD-1");

OrderQueryHandler queries = new OrderQueryHandler(commands.getStore());
System.out.println(queries.getSummary("ORD-1").total);

Explicación

CQRS separa dos preocupaciones:

  • Modelo de Comandos: Optimizado para escrituras — valida reglas de negocio, mantiene invariantes, procesa cambios de estado
  • Modelo de Consultas: Optimizado para lecturas — desnormalizado, indexado, a menudo leído de una base de datos separada (ej. Elasticsearch para búsqueda, Redis para búsquedas rápidas)

Los dos modelos se sincronizan ya sea sincrónicamente (misma transacción) o asíncronamente (proyecciones impulsadas por eventos).

Variantes

VarianteDescripciónCaso de uso
Base de Datos ÚnicaModelos separados, misma base de datosCQRS simple; menor complejidad
Doble Base de DatosEscritura a SQL, lectura de NoSQL/búsquedaConsultas complejas; alta escala de lectura
Event Sourcing + CQRSLos eventos son la fuente de verdad; los modelos de lectura son proyeccionesTrails de auditoría; consultas temporales
Segregación de APIEndpoints REST/GraphQL separados para comandos y consultasMicroservicios; límites de equipo

Lo que funciona

  • Empieza simple — separa modelos dentro de la misma base de datos antes de introducir almacenamiento dual
  • Usa consistencia eventual para modelos de lectura cuando las proyecciones asíncronas sean aceptables
  • Versiona tus modelos de lectura cuando cambie el esquema de consulta
  • Monitorea el lag de proyección — asegura que los modelos de lectura no se queden demasiado atrás de las escrituras
  • Mantén los comandos pequeños y enfocados — un comando debería hacer una sola cosa

Errores comunes

  • Aplicar CQRS a aplicaciones CRUD simples donde un único modelo es suficiente
  • Permitir que el modelo de lectura evada reglas de negocio (la validación pertenece a los comandos)
  • Ignorar problemas de consistencia eventual en CQRS asíncrono
  • Sobre-ingeniería con bases de datos separadas antes de probar la necesidad
  • No manejar reconstrucciones de modelos de lectura cuando cambia la lógica de proyección

Troubleshooting

  • Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
  • Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
  • Tight coupling after refactoring: check that interfaces are stable and dependencies point inward. Use dependency inversion to break accidental coupling.
  • Tests break when the design changes: favor stable contracts over internal structure. Test observable behavior, not private helpers.
  • Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.

Errores Comunes en Producción

  • Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
  • Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
  • Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
  • Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
  • Ignorar modos de fallo que el patrón no cubre.
  • Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
  • Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
  • Carecer de observabilidad sobre rendimiento y propagación de errores del patrón.

Preguntas frecuentes

¿Es este patrón adecuado para proyectos pequeños?

Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.

¿Cómo se compara este patrón con alternativas?

Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.

¿Puedo aplicar este patrón parcialmente?

Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.