Arquitectura de Software
CQRS en .NET 10: Qué Resuelve y Cómo Implementarlo con MediatR y C# 14
Empiezas con un único modelo. Una misma clase entidad se encarga de mostrar listados de productos, procesar pedidos, generar reportes y alimentar el índice de búsqueda. Funciona al principio. Entonces el catálogo de productos crece a millones de elementos, el procesamiento de pedidos necesita flujos de validación complejos, y los reportes requieren vistas desnormalizadas que chocan con la estructura normalizada que tus operaciones de escritura exigen. Has llegado al muro contra el que toda aplicación en crecimiento termina chocando: las lecturas y las escrituras tienen requisitos fundamentalmente diferentes, y forzarlas a pasar por el mismo modelo crea una fricción que ralentiza a todo tu equipo. CQRS — Command Query Responsibility Segregation — aborda esta tensión directamente. Separa tu aplicación en dos caminos distintos: uno optimizado para cambiar estado, otro optimizado para leerlo. En este artículo aprenderás qué es realmente CQRS, qué problemas específicos resuelve, cuándo deberías y no deberías usarlo, y cómo implementarlo desde cero en .NET 10 con C# 14 usando MediatR como mecanismo de despacho.
Qué es CQRS
CQRS significa Command Query Responsibility Segregation (Segregación de Responsabilidad de Comandos y Consultas). El concepto tiene su origen en el principio Command Query Separation (CQS) de Bertrand Meyer, que establece que cada método debería ser un comando que ejecuta una acción o una consulta que devuelve datos, pero nunca ambas cosas. Greg Young extendió este principio del nivel de métodos al nivel arquitectónico, proponiendo que los modelos completos deberían estar separados: un modelo para escrituras (comandos), otro para lecturas (consultas).
La premisa fundamental detrás de CQRS es directa: lecturas y escrituras tienen requisitos no funcionales diferentes. Las operaciones de escritura necesitan validación, cumplimiento de reglas de negocio, consistencia transaccional y publicación de eventos de dominio. Las operaciones de lectura necesitan velocidad, proyecciones flexibles, formas de datos desnormalizadas y caché. Cuando fuerzas ambas preocupaciones en un único modelo, comprometes las dos. Tus consultas de lectura arrastran lógica de validación que no necesitan. Tus operaciones de escritura cargan campos de proyección que existen solo para propósitos de visualización.
Una aclaración fundamental: CQRS no es Event Sourcing. Estos dos patrones se mencionan frecuentemente juntos, pero son conceptos independientes. Puedes implementar CQRS con una base de datos relacional tradicional sin ningún event store. Event Sourcing es una estrategia de persistencia; CQRS es una estrategia de segregación. Combinarlos puede ser potente, pero adoptar CQRS no requiere adoptar Event Sourcing.
Perspectiva técnica: CQRS no exige bases de datos separadas. La implementación más simple utiliza una única base de datos con modelos de lectura y escritura separados en código. Puedes evolucionar hacia almacenes físicamente separados después si el rendimiento lo exige, pero comenzar con separación lógica entrega la mayoría de los beneficios arquitectónicos inmediatamente.
Escenario práctico: Considera una plataforma de comercio electrónico donde la página de listado de productos necesita mostrar nombre del producto, precio, calificación promedio, estado de stock e información del vendedor desde cinco tablas diferentes. El proceso de creación de pedidos necesita validar inventario, aplicar reglas de descuento, calcular impuestos y actualizar conteos de stock. Usar una única entidad Product para ambas operaciones significa que la consulta de listado carga propiedades de navegación innecesarias, mientras que el proceso de pedido arrastra campos que solo existen para visualización a través del pipeline de validación. CQRS elimina esta tensión dándole a cada camino exactamente el modelo que necesita.
Qué problemas resuelve CQRS
Optimización de lecturas y escrituras
En arquitecturas CRUD tradicionales, el mismo modelo de datos sirve tanto para operaciones de lectura como de escritura. Esto crea un compromiso fundamental: normalizas el esquema para la integridad de escritura, pero las lecturas sufren porque requieren joins costosos entre tablas normalizadas. Alternativamente, desnormalizas para rendimiento de lectura, pero las escrituras se complican porque debes mantener datos redundantes de forma consistente. CQRS elimina este trade-off por completo. El lado de escritura usa un modelo normalizado optimizado para consistencia y validación. El lado de lectura usa proyecciones desnormalizadas, DTOs planos, o incluso vistas precalculadas optimizadas para la forma exacta que cada componente de UI necesita. Ningún lado se compromete por el otro.
Escalabilidad independiente
La mayoría de las aplicaciones son intensivas en lectura. Una aplicación web típica maneja de diez a cien veces más solicitudes de lectura que de escritura. En un modelo monolítico, escalar lecturas significa escalar el modelo completo, incluyendo toda la complejidad del lado de escritura. CQRS te permite escalar los caminos de lectura y escritura independientemente. Puedes desplegar réplicas de lectura, agregar capas de caché al lado de consultas, o incluso usar una tecnología de base de datos diferente para lecturas, todo sin afectar las operaciones de escritura. El lado de comandos puede permanecer en un único nodo de base de datos fuertemente consistente mientras el lado de consultas se distribuye entre múltiples réplicas de lectura.
Separación de responsabilidades y responsabilidad única
Una única clase de servicio que maneja tanto CreateOrder como GetOrderDetails viola el Principio de Responsabilidad Única. La operación de creación necesita validación, autorización, lógica de dominio y publicación de eventos. La operación de lectura necesita proyección de datos, caché y paginación. Son responsabilidades fundamentalmente diferentes con frecuencias de cambio distintas. CQRS impone esta separación de forma estructural: los command handlers contienen solo lógica de escritura, los query handlers contienen solo lógica de lectura. Cuando las reglas de negocio para creación de pedidos cambian, modificas un command handler sin riesgo de efectos secundarios en las operaciones de lectura. Cuando un nuevo dashboard necesita una forma de datos diferente, agregas un query handler sin tocar la lógica de escritura.
Gestión de complejidad en dominios grandes
A medida que crece la complejidad del dominio, la brecha entre lo que necesitas escribir y lo que necesitas leer se amplía dramáticamente. Un sistema financiero de trading podría aceptar un simple PlaceOrderCommand con cinco campos pero mostrar una orden a través de un modelo de lectura que une doce tablas con datos de mercado en tiempo real, cálculos de riesgo y flags de compliance. Intentar servir ambos a través de un único método de repositorio crea métodos con docenas de parámetros, includes opcionales y lógica condicional. CQRS mantiene cada lado enfocado en su propio presupuesto de complejidad. Los command handlers manejan reglas de negocio. Los query handlers manejan ensamblaje de datos. Ninguno se ahoga en las preocupaciones del otro.
Perspectiva técnica: La separación de responsabilidades en CQRS también habilita el desarrollo paralelo por equipos. Un equipo puede trabajar en command handlers y lógica de dominio mientras otro equipo optimiza el rendimiento de consultas y construye nuevas proyecciones de lectura. El único contrato compartido es el esquema de base de datos, y hasta eso puede desacoplarse con sincronización basada en eventos entre almacenes de escritura y lectura.
Cuándo usar CQRS y cuándo no
CQRS agrega complejidad estructural. Esa complejidad se justifica solo cuando los problemas que resuelve están realmente presentes en tu aplicación. Usa CQRS cuando:
- Los modelos de lectura y escritura divergen significativamente en forma o volumen
- Necesitas escalabilidad independiente para lecturas y escrituras
- La lógica de dominio es suficientemente compleja para justificar command handlers dedicados
- Múltiples equipos trabajan en diferentes aspectos del mismo bounded context
- Necesitas auditoría o consultas temporales en el lado de escritura
- Los requisitos de rendimiento difieren entre los caminos de lectura y escritura
Evita CQRS cuando:
- Tu aplicación es un CRUD simple con mínima lógica de negocio
- Los modelos de lectura y escritura son casi idénticos
- Tu equipo es pequeño y la indirección adicional ralentiza el desarrollo
- El dominio es lo suficientemente simple para que un patrón de repositorio único sea suficiente
| Dimensión | CRUD tradicional | CQRS |
|---|---|---|
| Cantidad de modelos | Modelo único compartido | Modelos de lectura/escritura separados |
| Escalabilidad | Todo se escala junto | Lectura/escritura escalan independientemente |
| Complejidad | Baja inicial, crece rápido | Mayor inicial, crece linealmente |
| Optimización de rendimiento | Una solución para todo | Específica por camino |
| Paralelismo de equipos | Conflictos de merge en modelos compartidos | Flujos de trabajo independientes |
| Testing | Pesado en integración | Handlers testeables unitariamente |
| Ideal para | Dominios simples, equipos pequeños | Dominios complejos, apps intensivas en lectura |
Perspectiva técnica: Comienza sin CQRS. Construye tu aplicación con un patrón de repositorio simple. Cuando notes que tus DTOs de lectura divergen de tus entidades de escritura, que tus métodos de servicio contienen lógica condicional para "¿esto es una lectura o una escritura?", o que el rendimiento de tus consultas se degrada debido a esquemas optimizados para escritura — esa es la señal para refactorizar hacia CQRS. El patrón es más fácil de adoptar incrementalmente que de eliminar retroactivamente.
Implementando CQRS en .NET 10 con MediatR y C# 14
Estructura de proyecto en Clean Architecture
Una implementación limpia de CQRS se mapea naturalmente a las capas de Clean Architecture. Los comandos y consultas viven en la capa Application. Los handlers referencian entidades de dominio y abstracciones de infraestructura a través de interfaces. La capa API despacha solicitudes a través de MediatR sin conocer qué handler las procesa.
src/
├── MyApp.Domain/
│ ├── Entities/
│ │ └── Order.cs
│ ├── Enums/
│ │ └── OrderStatus.cs
│ └── Interfaces/
│ └── IOrderRepository.cs
├── MyApp.Application/
│ ├── Orders/
│ │ ├── Commands/
│ │ │ ├── CreateOrder/
│ │ │ │ ├── CreateOrderCommand.cs
│ │ │ │ ├── CreateOrderCommandHandler.cs
│ │ │ │ └── CreateOrderCommandValidator.cs
│ │ │ └── UpdateOrder/
│ │ │ ├── UpdateOrderCommand.cs
│ │ │ └── UpdateOrderCommandHandler.cs
│ │ └── Queries/
│ │ └── GetOrderById/
│ │ ├── GetOrderByIdQuery.cs
│ │ ├── GetOrderByIdQueryHandler.cs
│ │ └── OrderDto.cs
│ └── Common/
│ └── Behaviors/
│ └── ValidationBehavior.cs
├── MyApp.Infrastructure/
│ ├── Persistence/
│ │ ├── AppDbContext.cs
│ │ └── Repositories/
│ │ └── OrderRepository.cs
│ └── DependencyInjection.cs
└── MyApp.Api/
├── Program.cs
└── Endpoints/
└── OrderEndpoints.cs
Definiendo comandos y consultas
Los comandos representan intenciones de cambiar estado. Las consultas representan solicitudes de datos. Ambos son portadores de datos simples — los records de C# 14 son ideales porque proporcionan semántica de valor, inmutabilidad y sintaxis concisa.
public record CreateOrderCommand(
Guid CustomerId,
string ShippingAddress,
List<OrderItemDto> Items) : IRequest<CreateOrderResult>;
public record CreateOrderResult(Guid OrderId, decimal TotalAmount);
public record GetOrderByIdQuery(Guid OrderId) : IRequest<OrderDto?>;
public record OrderDto(
Guid Id,
string CustomerName,
string ShippingAddress,
decimal TotalAmount,
string Status,
DateTime CreatedAt,
List<OrderItemDto> Items);
Observa la asimetría: el comando lleva solo los datos necesarios para crear un pedido. La consulta retorna un DTO rico con campos calculados y datos unidos que el comando nunca necesita. Esta es la esencia de CQRS — cada camino lleva exactamente los datos que requiere.
Implementación del command handler
Los command handlers contienen la lógica de negocio del lado de escritura. Validan reglas de negocio, modifican entidades de dominio y persisten cambios. Usando constructores primarios de C# 14, los handlers se mantienen concisos.
public sealed class CreateOrderCommandHandler(
IOrderRepository orderRepository,
IUnitOfWork unitOfWork)
: IRequestHandler<CreateOrderCommand, CreateOrderResult>
{
public async Task<CreateOrderResult> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
var order = Order.Create(
request.CustomerId,
request.ShippingAddress);
foreach (var item in request.Items)
{
order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
}
orderRepository.Add(order);
await unitOfWork.SaveChangesAsync(cancellationToken);
return new CreateOrderResult(order.Id, order.TotalAmount);
}
}
Implementación del query handler
Los query handlers se enfocan exclusivamente en leer datos. Emplean estrategias optimizadas para lectura: AsNoTracking() para omitir la detección de cambios de Entity Framework, proyección con Select para cargar solo las columnas necesarias, y mapeo directo a DTOs para evitar materializar grafos de entidades completos.
public sealed class GetOrderByIdQueryHandler(
AppDbContext context)
: IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
public async Task<OrderDto?> Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
return await context.Orders
.AsNoTracking()
.Where(o => o.Id == request.OrderId)
.Select(o => new OrderDto(
o.Id,
o.Customer.Name,
o.ShippingAddress,
o.TotalAmount,
o.Status.ToString(),
o.CreatedAt,
o.Items.Select(i => new OrderItemDto(
i.ProductId,
i.ProductName,
i.Quantity,
i.UnitPrice
)).ToList()
))
.FirstOrDefaultAsync(cancellationToken);
}
}
Perspectiva técnica: Observa que el query handler inyecta AppDbContext directamente en lugar de pasar por un repositorio. Esto es intencional. Los repositorios aportan valor en el lado de escritura al encapsular la lógica de persistencia e imponer límites de agregados. En el lado de lectura, los repositorios agregan abstracción innecesaria porque las consultas son proyecciones, no operaciones de dominio. Consultar el DbContext directamente te da acceso completo a proyecciones LINQ, joins y optimizaciones que un patrón de repositorio necesitaría exponer a través de firmas de métodos cada vez más complejas.
Configuración de MediatR y pipeline
El registro en Program.cs conecta todo mediante una única llamada a AddMediatR, que escanea el ensamblado buscando todos los handlers y registra pipeline behaviors en orden.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<CreateOrderCommandHandler>();
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
builder.Services.AddValidatorsFromAssemblyContaining<CreateOrderCommandValidator>();
var app = builder.Build();
app.MapPost("/api/orders", async (CreateOrderCommand command, IMediator mediator) =>
{
var result = await mediator.Send(command);
return Results.Created($"/api/orders/{result.OrderId}", result);
});
app.MapGet("/api/orders/{id:guid}", async (Guid id, IMediator mediator) =>
{
var order = await mediator.Send(new GetOrderByIdQuery(id));
return order is not null ? Results.Ok(order) : Results.NotFound();
});
app.Run();
Escenario práctico: Una startup fintech migró de una capa de servicios monolítica con 47 métodos a CQRS con MediatR. Cada handler se convirtió en una unidad enfocada con una única responsabilidad. Su cobertura de tests saltó del 34% al 82% porque los handlers eran testeables independientemente sin necesidad de mockear la mitad de la aplicación. Las code reviews mejoraron porque cada pull request modificaba exactamente un handler, haciendo que el alcance del cambio fuera inmediatamente evidente.
Flujo CQRS: De la solicitud a la respuesta
Integración con Clean Architecture
CQRS y Clean Architecture son socios naturales. Clean Architecture proporciona los límites entre capas; CQRS proporciona el patrón estructural para cómo fluyen las operaciones a través de esas capas. La capa Application se convierte en el corazón de CQRS, alojando todos los comandos, consultas, sus handlers y las interfaces que definen las dependencias de infraestructura.
La capa Domain permanece pura: entidades, value objects y eventos de dominio con cero conocimiento de la mecánica CQRS. La capa Application contiene la infraestructura CQRS: comandos con sus handlers y validadores, consultas con sus handlers y DTOs, y pipeline behaviors como validación y logging. La capa Infrastructure implementa las interfaces de repositorio definidas en Domain y proporciona el DbContext usado por los query handlers. La capa API se mantiene delgada: recibe solicitudes HTTP, construye el comando o consulta apropiado, lo despacha a través de MediatR y retorna el resultado.
Esta separación significa que la capa Application puede testearse completamente en aislamiento. Los command handlers se testean unitariamente con repositorios mockeados. Los query handlers se testean con una base de datos en memoria. Los pipeline behaviors se testean verificando que intercepten y validen correctamente las solicitudes. Ninguno de estos tests requiere un servidor HTTP, una base de datos real, ni ninguna dependencia de infraestructura.
Escenario práctico: Un equipo de desarrollo reestructurando una aplicación monolítica de comercio electrónico hacia CQRS con Clean Architecture descubrió que su mayor ganancia inicial no fue el rendimiento — fue la incorporación de nuevos desarrolladores. Los nuevos miembros del equipo podían mirar la carpeta Application/Orders/Commands y entender inmediatamente cada operación de escritura que el sistema soporta. La estructura de carpetas se convirtió en documentación viva. Anteriormente, las mismas operaciones estaban dispersas entre tres clases de servicio que sumaban 2,400 líneas, y entender el sistema requería conocimiento tribal.
Integración con FluentValidation
CQRS separa comandos de consultas. FluentValidation asegura que los comandos lleven datos válidos antes de llegar a los handlers. El punto de integración es IPipelineBehavior<TRequest, TResponse> de MediatR, que intercepta cada solicitud y ejecuta todos los validadores registrados antes de permitir que el handler se ejecute.
public sealed class ValidationBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (!validators.Any())
return await next(cancellationToken);
var context = new ValidationContext<TRequest>(request);
var results = await Task.WhenAll(
validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var errors = results
.SelectMany(r => r.Errors)
.Where(e => e is not null)
.ToList();
if (errors.Count is not 0)
throw new ValidationException(errors);
return await next(cancellationToken);
}
}
Cada comando obtiene su propia clase validadora. Un CreateOrderCommandValidator valida los campos de creación de pedido. El behavior ejecuta todas las instancias registradas de IValidator<CreateOrderCommand> automáticamente. Ningún command handler necesita verificar validación — el pipeline garantiza que solo comandos válidos lleguen.
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty().WithMessage("El cliente es obligatorio.");
RuleFor(x => x.ShippingAddress)
.NotEmpty().WithMessage("La dirección de envío es obligatoria.")
.MaximumLength(500);
RuleFor(x => x.Items)
.NotEmpty().WithMessage("El pedido debe contener al menos un artículo.");
}
}
Para una inmersión profunda en FluentValidation, incluyendo validación asíncrona, estrategias de testing y composición avanzada de reglas, consulta la guía completa de FluentValidation.
Consideraciones de rendimiento y escalabilidad
MediatR agrega una sobrecarga insignificante en tiempo de ejecución. El dispatcher usa reflexión al inicio para construir un mapa de handlers, pero el despacho de solicitudes en sí es una búsqueda en diccionario seguida de una invocación de delegado. En benchmarks, MediatR agrega menos de un microsegundo por solicitud comparado con llamadas directas a métodos. Este costo es invisible junto a cualquier operación de base de datos o llamada de red en tu pipeline.
Las ganancias reales de rendimiento en CQRS provienen de optimizar cada camino independientemente. En el lado de consultas, usa AsNoTracking() con Entity Framework para omitir la detección de cambios, reduciendo la asignación de memoria y el uso de CPU hasta un 40% para operaciones de lectura. Para escenarios de lectura de alto rendimiento, considera Dapper para consultas SQL directas que bypasean EF por completo. Las réplicas de lectura en SQL Server o PostgreSQL pueden distribuir la carga de consultas entre múltiples instancias de base de datos sin afectar la consistencia de escritura.
En el lado de comandos, los handlers deberían enfocarse en un único agregado. Cargar el estado mínimo necesario, aplicar lógica de dominio y persistir cambios a través de IUnitOfWork mantiene las transacciones de escritura cortas y reduce la contención de bloqueos. Evita cargar grafos de objetos grandes para operaciones que solo modifican una única entidad.
El caching se vuelve directo con CQRS. Los resultados de consultas son naturalmente cacheables porque representan snapshots puntuales sin efectos secundarios. Los resultados de comandos nunca deberían cachearse porque representan cambios de estado. Esta distinción clara elimina la complejidad de invalidación de caché que afecta a las arquitecturas CRUD donde un único método tanto lee como escribe.
Para despliegues cloud-native, CQRS habilita escalamiento dirigido. Despliega query handlers detrás de grupos de auto-escalado con caching agresivo. Despliega command handlers en instancias de cómputo con conexiones de base de datos rápidas y transaccionales. Cada camino escala según su patrón de demanda real, no el peor caso combinado de ambos.
Errores comunes de los desarrolladores
Después de revisar docenas de implementaciones CQRS en sistemas de producción, estos seis errores aparecen consistentemente:
- Incluir lógica de consulta dentro de command handlers. Un command handler que retorna un DTO completamente poblado con datos unidos está haciendo dos trabajos. Los comandos deberían retornar datos mínimos de confirmación — un ID y un status. Si el llamador necesita la entidad completa después de la creación, despacha una consulta separada.
- Usar la misma configuración de DbContext para lecturas y escrituras. El change tracker de Entity Framework agrega sobrecarga que las operaciones de lectura no necesitan. Configura un DbContext de solo lectura con
QueryTrackingBehavior.NoTrackingpor defecto para query handlers, y un contexto separado con tracking para command handlers. - Nombrar comandos según operaciones CRUD en lugar de intenciones de negocio.
UpdateProductCommandno te dice nada sobre la operación de negocio.AdjustProductPriceCommand,RetireProductCommandyRestockProductCommandllevan intención de negocio clara y pueden tener reglas de validación específicas. CQRS funciona mejor cuando los comandos reflejan el lenguaje del dominio. - Confundir CQRS con Event Sourcing. No necesitas un event store, event bus, ni consistencia eventual para implementar CQRS. Una única base de datos SQL Server con modelos de lectura y escritura separados en código entrega el 80% de los beneficios con el 20% de la complejidad.
- Segregación prematura de bases de datos. Comenzar con bases de datos de lectura y escritura separadas introduce desafíos de consistencia distribuida desde el primer día. Empieza con una única base de datos y modelos lógicamente separados. La separación física de bases de datos es una optimización que agregas cuando los datos de monitoreo demuestran que la necesitas.
- Saltarse el pipeline de validación. Sin un
ValidationBehavioren el pipeline de MediatR, cada handler debe validar su propia entrada. Esto lleva a validación inconsistente, verificaciones duplicadas y comandos inválidos llegando a la lógica de dominio. Siempre implementa la validación como un pipeline behavior.
Preguntas frecuentes
¿CQRS es solo para microservicios?
No. CQRS funciona igualmente bien en aplicaciones monolíticas. De hecho, a menudo es más fácil de implementar en un monolito porque compartes una única base de datos y unidad de despliegue. CQRS es un patrón arquitectónico a nivel de código, no un patrón de despliegue. Una aplicación monolítica .NET con MediatR, command y query handlers separados, y una única base de datos SQL Server es una implementación CQRS perfectamente válida y común.
¿Necesito bases de datos separadas para lecturas y escrituras?
No. La implementación CQRS más simple y común usa una única base de datos. Separas modelos en código: los comandos pasan por interfaces de repositorio que imponen límites de agregados, mientras que las consultas acceden al DbContext directamente con proyecciones. La separación física de bases de datos es una optimización opcional para requisitos de escala extremos, no un requisito del patrón.
¿Cómo funciona CQRS con datos en tiempo real?
Depende de tus requisitos de consistencia. Con una única base de datos, las lecturas son inmediatamente consistentes — las consultas ven las últimas escrituras confirmadas. Con bases de datos separadas, hay un retardo de propagación entre los almacenes de escritura y lectura. Para la mayoría de aplicaciones, esta consistencia eventual es aceptable (catálogos de productos, reportes). Para transacciones financieras o inventario donde lecturas obsoletas causan errores de negocio, mantente con una única base de datos o implementa mecanismos de compensación.
¿Puedo usar CQRS con Minimal APIs?
Sí, y es un ajuste natural. Cada endpoint de Minimal API se mapea directamente a un único comando o consulta despachado a través de MediatR. El endpoint recibe la solicitud, construye el comando o consulta, llama a mediator.Send() y retorna el resultado. Esto mantiene los endpoints delgados y enfocados, lo que se alinea perfectamente con la filosofía de Minimal APIs.
¿Cuál es la relación entre CQRS y DDD?
CQRS y Domain-Driven Design son complementarios pero independientes. DDD proporciona la metodología de modelado — agregados, value objects, bounded contexts. CQRS proporciona la separación operacional — los comandos modifican agregados, las consultas leen proyecciones. Puedes usar CQRS sin DDD (muchas aplicaciones lo hacen), y puedes usar DDD sin CQRS. Cuando se combinan, DDD define cómo luce tu dominio, y CQRS define cómo fluyen las operaciones a través de él.
Empieza a Separar Lo Que Nunca Debió Estar Junto
CQRS no es un framework para instalar ni una biblioteca para referenciar. Es una decisión estructural que reconoce una verdad fundamental: la forma en que lees datos y la forma en que los cambias son problemas diferentes que merecen soluciones diferentes. Con MediatR en .NET 10, la implementación es directa. Los comandos y consultas son records simples. Los handlers son clases enfocadas con una única responsabilidad. Los pipeline behaviors manejan preocupaciones transversales como la validación. Todo el patrón se mapea limpiamente a capas de Clean Architecture que tu equipo ya comprende.
El próximo paso práctico es concreto: escoge un endpoint en tu aplicación actual donde el modelo de lectura y el modelo de escritura hayan divergido claramente. Crea un record de comando para la operación de escritura y un record de consulta para la lectura. Implementa sus handlers. Conéctalos a través de MediatR. Inmediatamente sentirás la claridad de tener cada operación en su propia clase con sus propios tests y su propio ciclo de vida de cambios. Esa única refactorización te dirá más sobre CQRS que cualquier discusión teórica.
Una vez que tus comandos fluyan a través de MediatR, agrega un ValidationBehavior para interceptarlos con reglas de FluentValidation. La validación se convierte en una preocupación del pipeline, no algo disperso entre handlers. Tus handlers se mantienen enfocados en la lógica de negocio, y los comandos inválidos nunca los alcanzan.
Compartir este artículo
Suscríbete
Recibe los últimos artículos directamente en tu bandeja de entrada.
Deja un comentario