Skip to content
SP StackPractices
advanced

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

Patrón CQRS

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

Mejores prácticas

  • 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

Preguntas frecuentes

P: ¿CQRS requiere Event Sourcing? R: No. CQRS puede usarse con cualquier modelo de persistencia. Event Sourcing a menudo se empareja con CQRS porque los eventos hacen datos de origen naturales para proyecciones de modelos de lectura, pero son patrones independientes.

P: ¿Cómo mantengo los modelos de lectura y escritura sincronizados? R: En CQRS síncrono, actualiza ambos en la misma transacción. En CQRS asíncrono, publica eventos después de escrituras y haz que los consumidores reconstruyan el modelo de lectura. Acepta la consistencia eventual.

P: ¿Cuándo debería evitar CQRS? R: Evita CQRS para aplicaciones CRUD simples, equipos pequeños, o cuando las proporciones de lectura/escritura estén balanceadas. La complejidad agregada solo se justifica cuando los dos lados tienen necesidades fundamentalmente diferentes de escalado o modelado.