Patrón que separa las operaciones de lectura y escritura en modelos distintos, optimizando cada uno independientemente para rendimiento y escalabilidad.
CQRS (Command Query Responsibility Segregation) es un patrón arquitectónico que separa las operaciones de escritura (commands) de las de lectura (queries) en modelos completamente distintos. A diferencia de los sistemas CRUD tradicionales donde el mismo modelo maneja ambas operaciones, CQRS permite que cada lado use su propia base de datos, esquema, tecnología y estrategia de escalado.
El patrón se basa en el principio CQS (Command Query Separation) de Bertrand Meyer, pero lo extiende a nivel arquitectónico. Los commands modifican el estado del sistema sin retornar datos, mientras que las queries retornan datos sin modificar el estado. Esta separación permite optimizaciones específicas para cada tipo de operación.
CQRS no requiere necesariamente event sourcing, aunque ambos patrones se complementan naturalmente. Puede implementarse con bases de datos relacionales tradicionales, usando vistas materializadas o procesos de sincronización para mantener los modelos de lectura actualizados.
// Command side - Write model
interface CreateOrderCommand {
customerId: string;
items: OrderItem[];
shippingAddress: Address;
}
class OrderCommandHandler {
constructor(
private orderRepository: OrderRepository,
private eventBus: EventBus
) {}
async handle(command: CreateOrderCommand): Promise<void> {
const order = new Order(
command.customerId,
command.items,
command.shippingAddress
);
await this.orderRepository.save(order);
// Publish event for read model projection
await this.eventBus.publish(new OrderCreatedEvent(order));
}
}
// Query side - Read model
interface OrderSummaryQuery {
customerId: string;
status?: OrderStatus;
dateRange?: DateRange;
}
class OrderQueryHandler {
constructor(private readDatabase: ReadDatabase) {}
async handle(query: OrderSummaryQuery): Promise<OrderSummary[]> {
return this.readDatabase.query(`
SELECT
order_id,
customer_name,
total_amount,
status,
created_at
FROM order_summaries
WHERE customer_id = $1
AND ($2::text IS NULL OR status = $2)
AND created_at BETWEEN $3 AND $4
ORDER BY created_at DESC
`, [query.customerId, query.status, query.dateRange?.start, query.dateRange?.end]);
}
}
// Event handler for projection
class OrderProjectionHandler {
async on(event: OrderCreatedEvent): Promise<void> {
await this.readDatabase.execute(`
INSERT INTO order_summaries (
order_id, customer_id, customer_name,
total_amount, status, created_at
) VALUES ($1, $2, $3, $4, $5, $6)
`, [
event.orderId,
event.customerId,
event.customerName,
event.totalAmount,
'PENDING',
event.timestamp
]);
}
}Proyección inmediata: Actualiza el modelo de lectura en la misma transacción. Garantiza consistencia pero reduce rendimiento y acoplamiento.
Proyección eventual: Usa eventos asincrónicos para actualizar el modelo de lectura. Mayor rendimiento y desacoplamiento, pero requiere manejar consistencia eventual.
class OrderQueryService {
async getOrderWithFallback(orderId: string): Promise<OrderView> {
// Try read model first
const orderView = await this.readModel.findById(orderId);
if (orderView) {
return orderView;
}
// Fallback to write model if not yet projected
const order = await this.writeModel.findById(orderId);
if (!order) {
throw new OrderNotFoundError(orderId);
}
// Build view on-the-fly
return this.buildOrderView(order);
}
private buildOrderView(order: Order): OrderView {
return {
id: order.id,
customerName: order.customer.name,
status: order.status,
totalAmount: order.calculateTotal(),
createdAt: order.createdAt
};
}
}CQRS puede implementarse sin event sourcing usando sincronización de datos tradicional:
class OrderService {
async createOrder(command: CreateOrderCommand): Promise<void> {
const transaction = await this.database.beginTransaction();
try {
// Update write model
const order = await this.orderRepository.create(command, transaction);
// Update read model in same transaction
await this.orderSummaryRepository.create({
orderId: order.id,
customerId: order.customerId,
totalAmount: order.totalAmount,
status: order.status
}, transaction);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
}| Criterio | CQRS Recomendado | CRUD Tradicional |
|---|---|---|
| Ratio lectura/escritura | > 10:1 | < 5:1 |
| Complejidad de queries | Alta, múltiples vistas | Simple, CRUD básico |
| Requisitos de escalado | Independiente por operación | Escalado uniforme |
| Consistencia | Eventual aceptable | Inmediata requerida |
| Experiencia del equipo | Sistemas distribuidos | Aplicaciones monolíticas |
| Dominio de negocio | Complejo, múltiples contextos | Simple, un contexto |
Sobre-ingeniería: Aplicar CQRS a sistemas CRUD simples añade complejidad innecesaria. No todos los bounded contexts necesitan CQRS.
Proyecciones síncronas: Actualizar modelos de lectura sincrónicamente elimina los beneficios de escalabilidad y puede crear cuellos de botella.
Modelos de lectura normalizados: Mantener la normalización en modelos de lectura desperdicia la oportunidad de optimizar para queries específicas.
Falta de versionado: No versionar eventos o esquemas de proyección complica la evolución del sistema y las migraciones.
CQRS permite optimizar lecturas y escrituras independientemente, lo cual es crítico en sistemas donde los patrones de acceso difieren significativamente. En arquitecturas de microservicios, habilita que cada servicio optimice su modelo de datos para su dominio específico. La separación facilita el escalado horizontal: los modelos de lectura pueden replicarse geográficamente mientras que los de escritura se mantienen centralizados. Combinado con event-driven architecture, CQRS permite construir sistemas resilientes que pueden reconstruir el estado a partir de eventos, facilitando debugging, auditoría y análisis histórico.
Patrón arquitectónico donde los componentes se comunican mediante eventos asíncronos, permitiendo sistemas desacoplados, escalables y reactivos.
Enfoque de diseño de software que centra el desarrollo en el dominio del negocio, usando un lenguaje ubicuo compartido entre desarrolladores y expertos de dominio.
Patrón donde el estado de la aplicación se deriva de una secuencia inmutable de eventos, proporcionando auditoría completa y la capacidad de reconstruir el estado en cualquier punto del tiempo.
Patrón para gestionar transacciones distribuidas en microservicios mediante una secuencia de transacciones locales con acciones de compensación para manejar fallos.