Introducción a CQRS
Introduccion a CQRS
En este capitulo aprenderemos los fundamentos del patron CQRS, entendiendo que problema resuelve y cuando es apropiado utilizarlo.
Que es CQRS
CQRS son las siglas de Command Query Responsibility Segregation, que en espanol significa “Segregacion de Responsabilidad entre Comandos y Consultas”. Es un patron arquitectonico que propone una idea simple pero poderosa: usar modelos diferentes para leer datos y para escribir datos.
En una aplicacion tradicional, usamos las mismas clases, los mismos repositorios y la misma base de datos tanto para consultar informacion como para modificarla. CQRS rompe con esta convencion separando completamente estas dos responsabilidades.
El Problema que Resuelve CQRS
En aplicaciones tradicionales, usamos el mismo modelo para leer y escribir datos:
[Cliente] → [API] → [Modelo Unico] → [Base de Datos]
Este enfoque funciona bien para aplicaciones simples, pero genera problemas cuando:
-
Las lecturas superan ampliamente a las escrituras: En la mayoria de aplicaciones, los usuarios consultan datos mucho mas frecuentemente de lo que los modifican. Un ratio de 100 lecturas por cada escritura es comun.
-
Los modelos de lectura requieren agregaciones complejas: Para mostrar un dashboard, por ejemplo, necesitas combinar datos de multiples tablas, calcular totales y promedios. Estas operaciones son costosas si se ejecutan cada vez que alguien consulta.
-
Las escrituras necesitan validaciones de negocio estrictas: Cuando un usuario crea un pedido, debes validar stock, verificar limites de credito, aplicar promociones. Estas reglas no tienen sentido al momento de consultar.
-
Se requiere escalar lecturas y escrituras independientemente: Si tienes miles de usuarios consultando pero pocas modificaciones, quieres escalar solo la parte de lectura sin afectar la de escritura.
La Solucion: Separar Lectura de Escritura
CQRS propone usar dos modelos completamente separados:
Escrituras: [Command] → [Command Handler] → [Write Model] → [DB Transaccional]
↓
[Eventos]
↓
Lecturas: [Query] → [Query Handler] → [Read Model] → [DB Optimizada]
Analicemos cada componente del diagrama:
-
Command: Es una instruccion para modificar datos. Por ejemplo, “Crear Pedido” o “Cancelar Suscripcion”. Los comandos expresan la intencion del usuario de cambiar algo en el sistema.
-
Command Handler: Es el componente que recibe un comando y ejecuta la logica necesaria para procesarlo. Valida las reglas de negocio y persiste los cambios.
-
Write Model: Es el modelo de datos optimizado para escrituras. Contiene todas las validaciones y reglas de negocio. Se almacena en una base de datos transaccional como PostgreSQL.
-
Eventos: Cuando el Write Model cambia, emite eventos que describen lo que ocurrio. Por ejemplo, “Pedido Creado” o “Item Agregado al Pedido”.
-
Query: Es una solicitud de informacion. Por ejemplo, “Obtener Pedido por ID” o “Buscar Pedidos del Cliente”. Las queries nunca modifican datos.
-
Query Handler: Recibe una query y la ejecuta contra el Read Model, retornando los datos solicitados.
-
Read Model: Es el modelo de datos optimizado para lecturas. Puede estar desnormalizado (datos duplicados) para evitar JOINs costosos. Puede usar bases de datos especializadas como Elasticsearch.
Proyecto OrderFlow
A lo largo del tutorial construiremos OrderFlow, un sistema e-commerce con:
- Gestión de pedidos
- Inventario
- Catálogo de productos
Estructura del Dominio
// TypeScript - Entidades principales
interface Order {
id: string;
customerId: string;
items: OrderItem[];
status: OrderStatus;
total: number;
createdAt: Date;
}
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
// Go - Entidades principales
type Order struct {
ID string
CustomerID string
Items []OrderItem
Status OrderStatus
Total float64
CreatedAt time.Time
}
type Product struct {
ID string
Name string
Price float64
Stock int
}
# Python - Entidades principales
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
@dataclass
class Order:
id: str
customer_id: str
items: list["OrderItem"]
status: "OrderStatus"
total: Decimal
created_at: datetime
@dataclass
class Product:
id: str
name: str
price: Decimal
stock: int
Cuándo Usar CQRS
Apropiado:
- Sistemas con alta disparidad lectura/escritura
- Dominios complejos con reglas de negocio elaboradas
- Necesidad de escalar lecturas independientemente
- Equipos grandes trabajando en paralelo
No apropiado:
- CRUD simples
- Aplicaciones pequeñas sin complejidad de dominio
- Equipos sin experiencia en arquitecturas distribuidas
Beneficios
- Escalabilidad independiente: Escala lecturas sin afectar escrituras
- Modelos optimizados: Cada modelo se diseña para su propósito
- Rendimiento: Read models desnormalizados para consultas rápidas
- Separación de concerns: Equipos pueden trabajar en paralelo
Desafíos
- Consistencia eventual: Los read models no reflejan cambios inmediatamente
- Complejidad: Más componentes que mantener
- Sincronización: Mantener read models actualizados
Proximos Pasos
En el siguiente capitulo exploraremos como separar las responsabilidades de lectura y escritura de forma practica.
Glosario
CQRS (Command Query Responsibility Segregation)
Definicion: Patron arquitectonico que separa las operaciones de lectura (queries) de las operaciones de escritura (commands) en modelos de datos distintos e independientes.
Por que es importante: Permite optimizar cada modelo para su proposito especifico. El modelo de escritura puede enfocarse en validaciones y consistencia, mientras el modelo de lectura se optimiza para consultas rapidas.
Ejemplo practico: Imagina una tienda online. Cuando un cliente hace un pedido (escritura), necesitas validar stock, calcular impuestos y verificar pago. Pero cuando otro cliente ve el catalogo (lectura), solo necesitas mostrar productos con sus precios. Son necesidades muy diferentes que CQRS permite manejar con modelos especializados.
Command (Comando)
Definicion: Objeto que representa una intencion de modificar el estado del sistema. Contiene toda la informacion necesaria para ejecutar una accion especifica.
Por que es importante: Encapsula la intencion del usuario de forma explicita y transportable. Permite validar, registrar y procesar las solicitudes de cambio de manera uniforme.
Ejemplo practico: Un comando CreateOrderCommand contiene el ID del cliente, los productos a comprar y la direccion de envio. Este comando viaja desde la API hasta el handler que lo procesa.
Query (Consulta)
Definicion: Objeto que representa una solicitud de informacion al sistema. Define que datos se necesitan y con que criterios buscarlos.
Por que es importante: Permite expresar consultas como objetos tipados, facilitando la validacion, el cache y la optimizacion de las busquedas.
Ejemplo practico: Una query GetOrdersByCustomerQuery solicita todos los pedidos de un cliente especifico. El query handler la ejecuta contra el Read Model y retorna los resultados.
Write Model (Modelo de Escritura)
Definicion: Modelo de datos disenado para operaciones de escritura. Contiene las entidades de dominio con sus reglas de negocio y validaciones.
Por que es importante: Al separarlo del modelo de lectura, puede enfocarse exclusivamente en mantener la integridad y consistencia de los datos, sin preocuparse por optimizar consultas.
Ejemplo practico: El Write Model de un pedido incluye metodos como addItem() que valida stock disponible, o confirm() que verifica que el pedido no este vacio.
Read Model (Modelo de Lectura)
Definicion: Modelo de datos optimizado para consultas rapidas. Suele estar desnormalizado, con datos precalculados y estructurado para los casos de uso de visualizacion.
Por que es importante: Al estar separado del Write Model, puede duplicar datos, precalcular totales y usar estructuras que harian imposible mantener consistencia en escritura, pero que aceleran dramaticamente las lecturas.
Ejemplo practico: El Read Model de un pedido incluye el nombre del cliente (duplicado de otra tabla) y el total precalculado, evitando JOINs y calculos en cada consulta.
Handler (Manejador)
Definicion: Componente que recibe un Command o Query y ejecuta la logica correspondiente. Cada comando o query tiene su propio handler especializado.
Por que es importante: Aisla la logica de procesamiento en unidades pequenas y focalizadas. Facilita el testing y permite agregar funcionalidades transversales como logging o autorizacion.
Ejemplo practico: Un CreateOrderHandler recibe el comando de crear pedido, crea la entidad Order, la persiste en la base de datos y publica los eventos correspondientes.
Evento de Dominio
Definicion: Objeto que representa algo que ya ocurrio en el sistema. Describe un cambio de estado en tiempo pasado.
Por que es importante: Permite desacoplar el Write Model del Read Model. Cuando algo cambia en escritura, se emite un evento que las proyecciones consumen para actualizar los Read Models.
Ejemplo practico: Cuando se confirma un pedido, se emite el evento OrderConfirmedEvent que contiene el ID del pedido y su total. Las proyecciones escuchan este evento para actualizar el Read Model.