CQRS — Segregación de Responsabilidades de Comandos y Consultas
Guía completa 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
- 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.
Recursos Relacionados
Event Sourcing — State as a Sequence of Events
A deep dive into Event Sourcing: persist state changes as events, reconstruct aggregates from history, and build audit trails by design.
GuideCQRS + Event Sourcing — Combined Guide
A practical guide to combining CQRS and Event Sourcing: separating read and write models, rebuilding state from events, and handling eventual consistency.
GuideHexagonal Architecture — Ports, Adapters, and Testability
A complete guide to Hexagonal Architecture (Ports and Adapters): structure applications so domain logic is isolated from frameworks, databases, and external services.
GuideClean Architecture — The Dependency Rule and Layered Boundaries
A practical guide to Uncle Bob's Clean Architecture: organize code into layers so that frameworks, UI, and databases are details, not dependencies.