CQRS es un patrón arquitectónico propuesto por Greg Young (basado en el principio CQS de Bertrand Meyer) que establece una separación fundamental: las operaciones que modifican el estado del sistema (Commands) deben estar completamente separadas de las operaciones que lo leen (Queries). En términos prácticos: un mismo objeto, servicio o método NO puede hacer ambas cosas. Si escribe, no lee. Si lee, no escribe.
Un método debería o bien cambiar el estado de un objeto, o bien retornar un valor pero nunca ambas cosas al mismo tiempo.
Bertrand Meyer (CQS)
Cómo funciona: el flujo completo
La separación en CQRS crea dos pipelines paralelos dentro de la aplicación. Cada uno está optimizado para su propósito específico:
-
El lado Command (escritura): Cuando un usuario realiza una acción, crear un pedido, actualizar un perfil, confirmar un pago, etc., se genera un Command. Este Command es un objeto que representa la intención del usuario. El Command Bus lo enruta al Command Handler correspondiente, que valida, ejecuta la lógica de negocio, actualiza el Write Store y opcionalmente publica eventos de dominio.
-
El lado Query (lectura): Cuando se necesita mostrar datos, listar pedidos, mostrar un dashboard, obtener detalles de un producto, se genera una Query. El Query Handler la procesa leyendo directamente del Read Store, que está optimizado exclusivamente para consultas rápidas. No hay lógica de negocio aquí, solo proyecciones de datos.
-
La sincronización: El Read Store se mantiene actualizado escuchando los eventos que publica el Command Side. Esto es consistencia eventual: el Read Store puede estar ligeramente desactualizado por milisegundos, pero escala de forma independiente y puede ser tan simple como una caché Redis o tan complejo como un modelo desnormalizado en PostgreSQL.
Implementación con Cortex.Mediator
Commands
PlaceOrderCommand.cs
using Cortex.Mediator.Commands;
// Command — representa una intención de cambio
public record PlaceOrderCommand(Guid CustomerId, List Items) : ICommand;
// Handler — contiene toda la lógica de negocio
public sealed class PlaceOrderHandler(IOrderRepository repo, IEventBus events) : ICommandHandler
{
public async Task Handle(PlaceOrderCommand command, CancellationToken ct)
{
var order = Order.Place(command.CustomerId, command.Items);
await repo.SaveAsync(order, ct);
await events.PublishAsync(new OrderPlacedEvent(order.Id), ct);
return new PlaceOrderResult(order.Id);
}
}
Queries
GetOrdersQuery.cs
// Query — solo lectura, sin efectos secundarios
public record GetOrdersByCustomerQuery(Guid CustomerId) : IQuery>;
// Handler — va directo al Read Store, sin lógica de negocio
public sealed class GetOrdersByCustomerHandler(IReadDbContext readDb) : IQueryHandler>
{
public async Task> Handle(GetOrdersByCustomerQuery query, CancellationToken ct)
{
return await readDb.OrderSummaries
.Where(o => o.CustomerId == query.CustomerId)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(ct);
}
}
Controller
OrdersController.cs
[ApiController, Route("api/orders")]
public class OrdersController(IMediator mediator) : ControllerBase
{
[HttpPost]
public async Task Place(PlaceOrderCommand cmd, CancellationToken ct)
=> Ok(await mediator.SendCommandAsync(cmd, ct));
[HttpGet("customer/{customerId:guid}")]
public async Task GetByCustomer(Guid customerId, CancellationToken ct)
=> Ok(await mediator.SendQueryAsync(new GetOrdersByCustomerQuery(customerId), ct));
}
Ventajas y Desventajas
Ventajas en detalle
-
Escalabilidad independiente: El Read Side suele recibir 10x más tráfico que el Write Side. CQRS permite escalar cada lado de forma independiente, más réplicas de lectura, caché agresiva, sin tocar la lógica de escritura.
-
Modelos optimizados: El Write Model puede ser un modelo de dominio rico con invariantes y reglas. El Read Model puede ser un DTO completamente desnormalizado y aplanado, perfecto para mostrar en pantalla sin N+1 queries.
-
Alta testeabilidad: Cada Command Handler y Query Handler hace exactamente una cosa. Sus tests son directos, sin efectos secundarios inesperados.
-
Ideal con Event Sourcing: CQRS y Event Sourcing son compañeros naturales. El Write Store puede almacenar solo eventos; las proyecciones generan las vistas de lectura.
Desventajas a considerar
-
Consistencia eventual: Si usas dos almacenes separados, el Read Store puede estar brevemente desactualizado. Esto requiere que tu UI y tu lógica de negocio lo acepten.
-
Complejidad inicial: Para un equipo que viene de arquitecturas en capas, CQRS introduce muchos conceptos nuevos al mismo tiempo. La curva de aprendizaje es real.
-
Overhead en proyectos simples: Si tu aplicación es básicamente un CRUD sin lógica de negocio compleja, CQRS probablemente sea sobrearquitectura que ralentiza más de lo que ayuda.
¿Cuándo tiene sentido aplicarlo?
La pregunta no es '¿debería usar CQRS?' sino '¿tengo el problema que CQRS resuelve?'. Si tu sistema tiene un ratio de lectura/escritura muy asimétrico, lógica de negocio compleja en los comandos, o necesitas escalar de forma independiente cada lado CQRS paga la inversión. De lo contrario, es innecesaria.
| Escenario | Contexto | CQRS? |
|---|---|---|
| Alta carga de lectura | 10:1 reads vs writes | Ideal |
| CRUD simple | Formularios sin mucha lógica | No justifica |
| Lógica de negocio compleja | Muchas reglas en los commands | Ideal |
| MVP / Prototipo | Velocidad sobre arquitectura | Evitar |
| Event Sourcing | Historial completo de cambios | Natural |
| Sistema pequeño 1 dev | Poco tráfico, CRUD sencillo | Sobrearquitectura |
| Microservicios | Cada ms gestiona su modelo | Recomendado |
La trampa mas común...
Aplicar CQRS porque 'es buena práctica' sin analizar si el sistema realmente lo necesita. CQRS es una solución a problemas de escala y complejidad. Si no tienes esos problemas, introduces complejidad sin beneficio. Empieza simple y migra hacia CQRS cuando el dolor sea real.
CONCLUSION
CQRS no es magia, ni es una arquitectura que debas aplicar en todos los proyectos. Es una herramienta diseñada para un problema muy concreto: cuando la forma en que escribes datos y la forma en que los lees son tan diferentes que un solo modelo termina volviéndose confuso, pesado o difícil de mantener.
En escenarios adecuados como sistemas con alta carga de lectura, lógica de negocio compleja, microservicios o incluso Event Sourcing, CQRS puede mejorar significativamente la mantenibilidad, la escalabilidad y la claridad del código. En cambio, aplicado sin necesidad real, solo agrega más capas, más clases y más complejidad innecesaria.
La separación entre Commands y Queries no es solo una convención de nombres: es una decisión de diseño que influye directamente en cómo pruebas tu sistema, cómo crece con el tiempo y cómo el equipo entiende la lógica.
Al final, la pregunta no es “¿debería usar CQRS?”, sino:
“¿mi sistema realmente tiene el problema que CQRS resuelve?”
- Debes estar logueado para realizar comentarios