CQRS — Segregación de Responsabilidades de Comandos y
Referencia Detallada de CQRS: separa los modelos de lectura y escritura para optimizar rendimiento, escalabilidad y autonomía de equipos en dominios complejos.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
CQRS (Command Query Responsibility Segregation) es un patrón arquitectónico que separa los modelos utilizados para escribir datos de los modelos utilizados para leer datos. En lugar de un único modelo que maneja tanto comandos (escrituras) como consultas (lecturas), CQRS los divide en caminos distintos optimizados para sus respectivos propósitos. Esta separación permite ajustar rendimiento, escalar independientemente y simplificar los modelos mentales para dominios complejos.
When to Use
-
For alternatives, see Event Sourcing — State as a Sequence of Events.
-
Las cargas de lectura y escritura tienen requisitos fundamentalmente diferentes
-
Necesitas múltiples modelos de lectura para los mismos datos (ej. búsqueda, reportes, APIs)
-
Diferentes equipos poseen las lecturas vs las escrituras
-
Ya usas event sourcing (pareamiento natural)
-
Necesitas escalar lecturas y escrituras de forma independiente
When NOT to Use
- CRUD simple con patrones de lectura/escritura similares
- Equipos pequeños sin capacidad operacional para la complejidad adicional
- Sistemas donde la consistencia eventual es inaceptable en todas partes
Conceptos Core
Comandos
Los comandos representan intenciones de cambiar estado. Se nombran en imperativo y deben fallar rápido si la validación falla.
interface CreateOrderCommand {
customerId: string;
items: OrderItem[];
shippingAddress: Address;
}
Consultas
Las consultas retornan datos sin efectos secundarios. Se moldean por las necesidades de la UI o consumidor, no por el modelo de dominio.
interface OrderSummaryQuery {
customerId: string;
status?: OrderStatus;
page: number;
pageSize: number;
}
interface OrderSummary {
orderId: string;
total: Money;
status: OrderStatus;
placedAt: Date;
}
Modelo de Escritura
Optimizado para consistencia, validación y reglas de negocio. Generalmente mapea estrechamente al modelo de dominio.
Modelo de Lectura
Optimizado para rendimiento de consultas. Generalmente desnormalizado, proyectado y almacenado en una base de datos diferente.
CQRS Simple (Base de Datos Única)
┌─────────────┐ ┌──────────────┐
│ Comando │─────▶│ Modelo Escr │
│ Handler │ │ (ORM) │
└─────────────┘ └──────┬───────┘
│
┌──────┴───────┐
│ Base de │
│ Datos │
└──────┬───────┘
│
┌─────────────┐ ┌──────┴───────┐
│ Consulta │─────▶│ Modelo Lect │
│ Handler │ │ (DTO/Vista) │
└─────────────┘ └──────────────┘
// Lado escritura — modelo de dominio completo
class Order {
private items: OrderItem[] = [];
private status: OrderStatus = OrderStatus.PENDING;
addItem(product: Product, quantity: number): void {
if (quantity <= 0) throw new DomainError('Cantidad debe ser positiva');
this.items.push(new OrderItem(product, quantity));
}
confirm(): void {
if (this.items.length === 0) throw new DomainError('No se puede confirmar orden vacía');
this.status = OrderStatus.CONFIRMED;
}
}
// Lado lectura — DTO plano optimizado para listados
interface OrderListItem {
orderId: string;
customerName: string;
totalAmount: number;
itemCount: number;
status: string;
placedAt: string;
}
CQRS Avanzado (Almacenes Separados)
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Comando │─────▶│ Modelo Escr │─────▶│ Event Store│
│ Handler │ │ (Aggregate) │ │ (Eventos) │
└─────────────┘ └──────────────┘ └──────┬────┘
│
┌────┴────┐
│ Event │
│ Bus │
└───┬─────┘
│
┌─────────────┐ ┌──────────────┐ ┌─────┴────┐
│ Consulta │─────▶│ Modelo Lect │◀─────│ Projection│
│ Handler │ │ (NoSQL) │ │ Handler │
└─────────────┘ └──────────────┘ └───────────┘
Ejemplo de Proyección
class OrderProjectionHandler {
constructor(private readDb: ReadDatabase) {}
async handle(event: OrderEvent): Promise<void> {
switch (event.type) {
case 'OrderCreated':
await this.readDb.orders.insert({
orderId: event.orderId,
customerId: event.customerId,
total: event.items.reduce((sum, i) => sum + i.price * i.quantity, 0),
status: 'pending',
createdAt: event.timestamp
});
break;
case 'OrderConfirmed':
await this.readDb.orders.update(
{ orderId: event.orderId },
{ status: 'confirmed', confirmedAt: event.timestamp }
);
break;
}
}
}
Patrones de Optimización de Modelo de Lectura
| Patrón | Caso de Uso | Almacenamiento |
|---|---|---|
| Vista Materializada | Agregados pre-computados | Document DB |
| Índice de Búsqueda | Búsqueda full-text | Elasticsearch |
| Proyección de Grafo | Consultas de relaciones | Neo4j |
| Caché | Datos calientes | Redis |
| Stream de Eventos | Analítica en tiempo real | Kafka/Kinesis |
Modelos de Consistencia
- Consistencia fuerte — leer y escribir desde la misma transacción (CQRS simple)
- Consistencia eventual — modelo de lectura actualiza asincrónicamente (almacenes separados)
- Lee-tus-escrituras — enruta lecturas recientes al modelo de escritura temporalmente
Errores Comunes
- Separación prematura — agregar CQRS a CRUD simple añade complejidad sin beneficio
- Bugs de consistencia eventual — usuarios refrescan y no ven sus propias escrituras
- Explosión de modelos de lectura — mantener demasiadas proyecciones para cada caso de uso
- Infierno de transacciones distribuidas — intentar hacer almacenes separados fuertemente consistentes
FAQ
¿CQRS requiere Event Sourcing? No. Puedes usar CQRS con una base de datos relacional para lecturas y escrituras, o con bases de datos separadas. Event sourcing es un compañero natural pero no requerido.
¿Cómo manejo el lag en modelos de lectura? Usa el patrón lee-tus-escrituras, actualizaciones optimistas de UI, o polling con verificación de versión.
¿Puedo usar CQRS con microservicios? Sí. Cada servicio puede tener su propia separación lectura/escritura. Ten cuidado con consultas entre servicios — prefiere composición de API o vistas materializadas.
¿Cómo empiezo con esto en un proyecto existente?
Empieza con una parte pequeña y aislada de tu codebase. Aplica los conceptos de esta guía a un módulo o servicio. Mide el impacto, luego expande a otras áreas.
¿Qué herramientas necesito?
Las herramientas mencionadas throughout esta guía se listan en cada sección. La mayoría son open-source y ampliamente adoptadas. Consulta los recursos relacionados para instrucciones de setup.
¿Cómo mido el éxito después de implementar esto?
Define métricas claras antes de empezar: benchmarks de rendimiento, tasas de error o indicadores de mantenibilidad. Compara antes y después. Itera basándote en datos, no en suposiciones.
Temas Avanzados
Escenario Detallado: CQRS para Sistema de Ordenes E-commerce
Sistema: Gestion de ordenes e-commerce (TypeScript + Node.js)
Modelo escritura: PostgreSQL (normalizado, ACID)
Modelo lectura: Elasticsearch (desnormalizado, optimizado para busqueda)
Event bus: Kafka (proyecciones event-driven)
Lado escritura (command handlers):
POST /api/orders -> CreateOrderCommand -> Order aggregate -> OrderCreated event
PUT /api/orders/:id/confirm -> ConfirmOrderCommand -> Order aggregate -> OrderConfirmed event
PUT /api/orders/:id/cancel -> CancelOrderCommand -> Order aggregate -> OrderCancelled event
// Command handler
class CreateOrderHandler {
async handle(cmd: CreateOrderCommand): Promise<OrderId> {
const order = Order.create(cmd.customerId, cmd.items);
await this.orderRepo.save(order); // PostgreSQL
// Eventos despachados despues de guardar
return order.id;
}
}
// Aggregate aplica invariantes
class Order extends AggregateRoot {
static create(customerId: string, items: OrderItem[]): Order {
if (items.length === 0) throw new Error("Orden vacia");
if (items.length > 50) throw new Error("Max 50 items");
const order = new Order(OrderId.generate(), customerId);
order.status = OrderStatus.PENDING;
order.items = items;
order.total = items.reduce((s, i) => s + i.price * i.qty, 0);
order.raiseEvent(new OrderCreated(order.id, customerId, items, order.total));
return order;
}
}
Flujo de eventos:
OrderCreated event -> Kafka topic orders.events -> Projection handler
OrderConfirmed event -> Kafka topic orders.events -> Projection handler
OrderCancelled event -> Kafka topic orders.events -> Projection handler
Lado lectura (projection handlers):
class OrderProjection {
async handle(event: DomainEvent): Promise<void> {
switch (event.type) {
case "OrderCreated":
await this.es.index({
index: "orders",
id: event.orderId,
body: {
orderId: event.orderId,
customerId: event.customerId,
status: "pending",
total: event.total,
itemCount: event.items.length,
items: event.items, // Desnormalizado para lectura
createdAt: event.timestamp
}
});
break;
case "OrderConfirmed":
await this.es.update({
index: "orders",
id: event.orderId,
body: { doc: { status: "confirmed", confirmedAt: event.timestamp } }
});
break;
case "OrderCancelled":
await this.es.update({
index: "orders",
id: event.orderId,
body: { doc: { status: "cancelled", cancelledAt: event.timestamp } }
});
break;
}
}
}
Lado lectura (query handlers):
GET /api/orders/search?q=laptop -> query Elasticsearch
GET /api/orders?status=pending&customerId=123 -> filter Elasticsearch
GET /api/orders/:id -> Elasticsearch GET por ID
class OrderQueryHandler {
async searchOrders(query: string, page: number): Promise<OrderListItem[]> {
const result = await this.es.search({
index: "orders",
body: {
query: { multi_match: { query, fields: ["items.name", "orderId"] } },
from: (page - 1) * 20,
size: 20
}
});
return result.hits.hits.map(h => h._source);
}
}
Consistencia lee-tus-escrituras:
- Cuando un usuario crea una orden, retorna el order ID inmediatamente
- Frontend muestra orden optimista en la lista (desde estado local)
- Polling GET /api/orders/:id hasta que el status aparezca en Elasticsearch
- Lag tipico: 50-200ms (Kafka + proyeccion)
- Timeout: si no visible en 5s, fallback a query al modelo de escritura
Escalado:
Lado escritura: 2 instancias PostgreSQL (master + replica)
Lado lectura: 3 nodos Elasticsearch (sharded por orderId)
Proyeccion: 4 instancias consumer (Kafka consumer group)
Event bus: Kafka con 12 particiones
Metricas:
| Metrica | Target |
|---------|--------|
| Latencia escritura p95 | < 50ms |
| Latencia lectura p95 | < 20ms |
| Lag de proyeccion | < 500ms |
| Disponibilidad lectura | 99.95% |
| Disponibilidad escritura | 99.99% |
Como reconstruyo un modelo de lectura desde cero?
Si el modelo de lectura esta corrupto o necesita cambio de esquema, replay los eventos desde el event store. Deten los consumers de proyeccion, trunca el modelo de lectura, y replay todos los eventos desde el inicio. Para event stores grandes, usa replay basado en snapshots: procesa eventos en lotes de 10,000 con checkpoints. Alternativamente, ejecuta una “proyeccion sombra” junto a la existente, verifica consistencia de datos, luego cambia los query handlers al nuevo modelo de lectura.
Recursos Relacionados
Event Sourcing — Estado como Secuencia de Eventos
Inmersión profunda en Event Sourcing: persiste cambios de estado como eventos, reconstruye agregados desde el historial y construye audit trails por diseño.
GuideCQRS + Event Sourcing — Guía Combinada
Guía práctica de combinar CQRS y Event Sourcing: separar modelos de lectura y escritura, reconstruir estado desde eventos y manejar consistencia eventual.
GuideArquitectura Hexagonal — Puertos, Adaptadores y Testabilidad
Referencia Detallada de Arquitectura Hexagonal (Puertos y Adaptadores): estructura aplicaciones para aislar la lógica de dominio de frameworks, bases de datos y servicios externos.