02 · Fundamentos de CQRS
En este primer bloque de CQRS nos centraremos en los fundamentos esenciales que toda arquitectura debe contemplar antes de incorporar patrones más avanzados. Sin rodeos, analizaremos por qué dividir lecturas y escrituras, cómo se relacionan sus componentes clave y cómo se materializa este enfoque en un ejemplo real de dominio.
0. Dinámica Inicial: "El Dilema del Modelo Único"
Actividad Grupal:
Escenario de partida: Imaginemos que estamos diseñando la arquitectura backend para una plataforma de reservas de hoteles con 500.000 usuarios activos al día.
Actualmente, manejamos:
10.000 búsquedas por minuto (filtros complejos: ciudad, fecha, disponibilidad, precio, promociones, etc.)
200 reservas por minuto (creación de registros, bloqueo de habitación, transacciones, notificaciones)
Problemas a resolver:
Todo está bajo un modelo de datos único.
Usamos un enfoque clásico de repositorios CRUD sobre una única base de datos relacional.
La empresa quiere añadir un nuevo tipo de habitación: “habitaciones premium”, con políticas de disponibilidad distintas y reglas específicas de validación.
Preguntas a responder:
¿Qué problemas anticipan en términos de rendimiento, escalabilidad o mantenimiento?
¿Qué riesgos existen si queremos escalar solo el sistema de consultas?
¿Cómo afectaría una migración de esquema para añadir "habitaciones premium"?
1. Fundamentos Conceptuales
1.1 Anatomía de un Cuello de Botella
Demostración Visual:
Problema 1: Bloqueos por escrituras largas (ej: validación de stock) retrasan lecturas críticas.
Problema 2: Índices optimizados para reportes ralentizan transacciones (trade-off físico).
Caso Real:
Ejemplo Twitter: En 2012, su API de escritura (tweets) y lectura (timelines) colisionaban, requiriendo partición gradual.
1.2 CQRS como Separación de Intereses
Metáfora:
Cocina de restaurante:
Command Side: Chef (escribe) organiza estaciones para eficiencia en preparación.
Query Side: Mesero (lee) tiene menú desnormalizado para respuestas rápidas.
Principios Técnicos:
Segregación por Modelo:
Write Model: Agregados DDD + invariantes (ej:
InventoryAggregate.reserve()
).Read Model: Vistas específicas por caso de uso (ej:
StockDashboardView
).
Diagrama Comparativo:
1.3 No Todo es Oro: Trade-offs
Discusión Guiada:
✅ Ventajas:
Escalabilidad independiente (ej: Redis Cache para queries, Kafka para eventos).
Modelos especializados (ej: GraphQL para móvil, SQL para backoffice).
❌ Desventajas:
Complejidad eventual: Consistencia, sincronización de modelos.
Sobrecarga en despliegues (ej: Mantener 3 read models en MongoDB, ES, etc).
2. Arquitectura de Referencia
2.1 Componentes Clave con Responsabilidades
Deep Dive:
Command Handler:
Valida permisos, carga agregados, ejecuta lógica.
Código Ejemplo:
class ReserveStockHandler { async execute(command) { // 1. Validación de formato if (!command.payload.items) throw new InvalidCommandError(); // 2. Carga estado actual const order = await repo.load(command.orderId); // 3. Invoca dominio order.reserve(items); // 4. Persiste y publica await repo.save(order); await eventBus.publish(order.events); } }
Event Store:
Base de eventos inmutables (ej: EventStoreDB, DynamoDB Streams).
Patrón "Append-Only": Los eventos son fuentes de verdad.
2.2 Flujo End-to-End con Casos de Error
Secuencia con Fallos:
Mecanismos de Resiliencia:
Reintentos exponenciales en proyectores.
Dead Letter Queues + Monitorización.
Reconstrucción de Read Models desde Event Store.
3. Ejemplo Práctico: Dominio InventoryOrder
3.1 Live Coding: De CRUD a CQRS (10 min)
Antes (CRUD):
// Servicio monolítico
class InventoryService {
async reserveStock(orderId, items) {
const tx = await db.startTransaction();
try {
// 1. Bloquea filas
const stock = await tx.query('SELECT * FROM stock WHERE sku IN (...) FOR UPDATE');
// 2. Valida y actualiza
items.forEach(item => {
if (stock.find(s => s.sku === item.sku).available < item.qty)
throw new Error('Stock insuficiente');
stock.reserved += item.qty;
});
// 3. Actualiza múltiples tablas
await tx.update('stock', stock);
await tx.insert('reservations', { orderId, items });
await tx.commit();
} catch (err) {
await tx.rollback();
}
}
}
Problemas Identificados:
Bloqueos de largo alcance (FOR UPDATE).
Acoplamiento entre reservas y consultas.
Después (CQRS):
// Command Side
class ReserveStockCommandHandler {
constructor(private repo, private eventBus) {}
async execute(command) {
const order = await this.repo.load(command.orderId);
order.reserve(command.items);
await this.repo.save(order);
await this.eventBus.publish(order.events);
}
}
// Read Side (Proyección)
class StockProjector {
constructor(private readDb) {}
async onStockReserved(event) {
// Actualización eventualmente consistente
await this.readDb.collection('stock').updateMany(
{ sku: { $in: event.items.map(i => i.sku) } },
{ $inc: { reserved: item.qty } }
);
}
}
3.2 Simulación de Escalabilidad
Escenario:
100k reservas/hora vs 5M consultas de stock/día.
Configuración en AWS:
Command Side:
Lambda + DynamoDB (Event Store) + Reserved Concurrency.
Query Side:
API Gateway + ElasticCache (Redis) con TTL de 10 segundos.
ElasticSearch para búsquedas complejas.
Métrica Clave:
Escrituras: 500 ms/op (consistencia fuerte).
Lecturas: 5 ms/op (consistencia eventual).
3.3 Discusión de Patrones Relacionados
Event Sourcing: No es obligatorio, pero común en CQRS.
Sagas: Coordinación entre agregados vía eventos.
Caching Strategies:
Write-Through: Actualiza cache en comandos.
Refresh-Ahead: Pre-calcula consultas frecuentes.
4. Cierre y Preparación para la Próxima Sesión (5 min)
Resumen Visual:
Diagrama de CQRS:
Evaluación Rápida y ejercicios:
3 Preguntas Rápidas (Kahoot!):
¿Qué componente maneja la lógica de negocio en CQRS?
¿True or False?: CQRS requiere siempre Event Sourcing.
Nombra tres ventajas de separar modelos de lectura/escritura.
Última actualización