Arquitectura de software

CQRS: cuándo tiene sentido y cuándo es overengineering

CQRS no es MediatR ni dos bases de datos. Qué resuelve separar lecturas y escrituras, sus tres niveles de adopción y cuándo solo agrega complejidad.

Por Leyton Manuel Espitia Diaz 5 min de lectura

Fragmento de código C# con un comando que usa el modelo de dominio y una consulta con proyección

En muchos proyectos .NET, "aplicamos CQRS" significa que cada operación tiene una clase XCommand o XQuery y un handler que MediatR despacha. Eso puede ser útil, pero no es CQRS. Es una forma de organizar el código. CQRS es otra cosa, y entender la diferencia evita adoptar la complejidad sin recibir el beneficio.

Qué es CQRS realmente

Command Query Responsibility Segregation: usar modelos distintos para escribir y para leer.

La idea parte de una observación: las escrituras y las lecturas tienen necesidades opuestas.

  • Escribir necesita reglas: validar que el pedido tenga stock, que el cliente no supere su crédito, que el estado permita la transición. Necesita un modelo rico y consistente.
  • Leer necesita forma y velocidad: un listado con datos de cinco tablas, filtros, ordenamientos, totales. Necesita consultas a la medida de cada pantalla.

Cuando un solo modelo intenta servir a las dos cosas, las entidades se llenan de propiedades que solo usa una pantalla y las consultas cargan objetos complejos para mostrar tres columnas.

Nivel 1: separar en el código (el que casi siempre vale la pena)

La misma base de datos, el mismo DbContext, pero dos caminos distintos:

// Comando: carga el modelo de dominio y aplica las reglas
public async Task<Result> ShipOrderAsync(int orderId, CancellationToken ct)
{
    var order = await db.Orders.Include(o => o.Lines).FirstOrDefaultAsync(o => o.Id == orderId, ct);
    if (order is null) return Result.NotFound();

    var outcome = order.Ship(clock.UtcNow); // la regla vive en la entidad
    if (outcome.IsFailure) return outcome;

    await db.SaveChangesAsync(ct);
    return Result.Ok();
}

// Consulta: proyección directa a lo que la pantalla necesita, sin entidades
public Task<List<OrderListItem>> GetPendingAsync(int customerId, CancellationToken ct) =>
    db.Orders
      .Where(o => o.CustomerId == customerId && o.Status == OrderStatus.Pending)
      .Select(o => new OrderListItem(o.Id, o.CreatedAt, o.Lines.Sum(l => l.Quantity * l.UnitPrice)))
      .ToListAsync(ct);

Este nivel ya resuelve la mayor parte del problema: las reglas están en un lugar, las lecturas son eficientes y ninguna contamina a la otra. No necesita bibliotecas ni infraestructura nueva.

Nivel 2: modelos de lectura propios en la misma base

Cuando las consultas se vuelven costosas (un panel que agrega millones de filas en cada carga), se pueden mantener tablas o vistas diseñadas para leer:

  • Una vista indexada con los totales por cliente.
  • Una tabla de resumen que se actualiza cuando cambia un pedido.

Las lecturas se vuelven triviales. El costo: hay que mantener esos modelos sincronizados, y cada escritura hace un poco más de trabajo.

Nivel 3: almacenes separados

El modelo de escritura en SQL Server, y el de lectura en otro lugar (un motor de búsqueda, una réplica, una base documental), sincronizados con eventos. Es lo que muchas charlas presentan como "CQRS", y es el nivel más costoso:

  • Consistencia eventual. El usuario crea un pedido y, durante unos segundos, no aparece en el listado. Hay que diseñar la interfaz para eso.
  • Infraestructura adicional. Colas, procesadores de eventos, reintentos, monitoreo del retraso.
  • Más formas de fallar. Si un evento se pierde, los dos modelos dejan de coincidir y necesitas una forma de reconstruirlos.

Tiene sentido con requisitos concretos: búsquedas de texto completo sobre millones de registros, escalas de lectura muy superiores a las de escritura o vistas que combinan datos de varios sistemas.

CQRS no es MediatR

MediatR es una biblioteca que despacha un objeto a su handler. Puedes usarla para implementar CQRS, pero:

  • Puedes tener CQRS sin MediatR (como en el ejemplo del nivel 1).
  • Puedes usar MediatR sin CQRS: si tus queries cargan las mismas entidades que tus commands, solo tienes una forma diferente de llamar métodos.

Lo que define CQRS es si los modelos de lectura y escritura están separados, no cómo se invoca el código.

CQRS tampoco es Event Sourcing

Se presentan juntos con frecuencia, pero son independientes. Event Sourcing guarda la secuencia de eventos en lugar del estado actual. Puede combinarse con CQRS (los eventos alimentan los modelos de lectura), pero la gran mayoría de sistemas que se benefician de CQRS no necesitan Event Sourcing.

El trade-off

Nivel Beneficio Costo
1: separar en el código Reglas claras, lecturas eficientes Prácticamente ninguno
2: modelos de lectura Consultas complejas muy rápidas Mantener la sincronización
3: almacenes separados Escalar lecturas de forma independiente Consistencia eventual, infraestructura, operación

Cuándo tiene sentido

  • Nivel 1: casi siempre que haya reglas de negocio reales en las escrituras.
  • Nivel 2: cuando consultas concretas son lentas por agregaciones o combinaciones costosas, y el índice ya no alcanza.
  • Nivel 3: cuando hay requisitos medibles de escala o de búsqueda que la base transaccional no puede atender.

Cuándo es overengineering

  • En aplicaciones CRUD donde escribir y leer son casi la misma operación.
  • Cuando se adopta el nivel 3 "para estar preparados" sin un problema de escala medido.
  • Cuando el equipo no está preparado para operar consistencia eventual: los errores de sincronización son difíciles de depurar.

Conclusión

La versión más simple de CQRS, separar en el código cómo se escribe y cómo se lee, aporta casi todo su valor y no cuesta casi nada. Los niveles siguientes resuelven problemas reales de escala, pero solo vale la pena pagarlos cuando esos problemas existen. Es la misma lógica que aplicamos con Clean Architecture: primero el problema, después la estructura.

Leyton Manuel Espitia Diaz

Escrito por

Leyton Manuel Espitia Diaz

CEO y fundador de Nakanaba

Desarrollador de software y fundador de Nakanaba. Lidera la arquitectura de Vitrin: backend en .NET, aplicaciones en Angular y .NET MAUI, SQL Server y servicios en Cloudflare.

¿Tu sistema necesita una revisión de arquitectura?

Te ayudamos a decidir qué estructura necesita tu software hoy y cómo prepararlo para crecer, sin sobreingeniería.

Ver desarrollo a medida