La mayoría de las aplicaciones leen y escriben las mismas tablas a través del mismo modelo. Eso funciona hasta que las formas divergen: las escrituras necesitan reglas de negocio estrictas sobre un pedido a la vez, mientras que las lecturas quieren listas, búsquedas y totales sobre miles de pedidos. CQRS separa ambas cosas, y el event sourcing cambia lo que guardas desde el principio.
Suelen presentarse como pareja, pero son decisiones separadas. Este artículo explica cada una, cómo encajan, cómo es el diagrama de un sistema que combina ambas y los costes que hacen que muchos equipos se arrepientan de haberlas adoptado demasiado pronto.
CQRS en un párrafo
Command Query Responsibility Segregation significa usar un modelo para cambiar los datos y otro distinto para leerlos. Los comandos (PlaceOrder, CancelOrder) van a un lado de escritura que aplica las reglas. Las consultas van a un lado de lectura con la forma que necesitan las pantallas que las usan. El artículo de Martin Fowler sobre CQRS, que atribuye el patrón a Greg Young, incluye también la advertencia que vale la pena repetir: para la mayoría de los sistemas, CQRS añade una complejidad arriesgada. Úsalo donde las lecturas y las escrituras divergen de verdad, no en toda la aplicación.
CQRS no exige dos bases de datos. Un modelo de escritura y un conjunto de vistas SQL sobre las mismas tablas ya es CQRS. Los almacenes separados llegan después, cuando la carga o la forma de las lecturas los exigen.
Event sourcing en un párrafo
El event sourcing guarda los cambios en lugar del estado actual. Un pedido no es una fila con una columna de estado; es un flujo de eventos: OrderPlaced, PaymentAuthorized, OrderShipped. El estado actual es lo que obtienes al reproducirlos. El artículo de Fowler sobre event sourcing describe la idea en detalle. Obtienes gratis un historial de auditoría completo y puedes construir nuevas vistas del pasado en cualquier momento, porque no se sobrescribió nada.
Cómo encajan
El event sourcing casi obliga a usar CQRS. Reproducir eventos es una buena forma de cargar un pedido antes de cambiarlo, pero una forma pésima de responder a «muéstrame todos los pedidos abiertos de más de 100 dólares». Por eso el lado de escritura trabaja con flujos de eventos y el lado de lectura mantiene modelos de lectura separados que se actualizan a partir de esos eventos.
El flujo:
- Llega un comando al lado de comandos.
- El handler carga el agregado (el pedido) reproduciendo su flujo, comprueba las reglas de negocio y decide qué eventos nuevos emitir.
- Añade los eventos al event store con la versión esperada del flujo. Si otro comando añadió eventos mientras tanto, la operación falla y el comando se reintenta. Esto es concurrencia optimista, y sustituye a los bloqueos de fila.
- Los proyectores se suscriben al flujo de eventos y actualizan los modelos de lectura: una tabla para la lista de pedidos, un índice de búsqueda, contadores para un panel.
- Las consultas solo leen de los modelos de lectura.
El event store
Un event store necesita pocas funciones, pero las necesita exactamente:
- Flujos de solo anexado, uno por agregado, con un número de versión por evento.
- Anexado condicional sobre la versión esperada, de forma atómica.
- Lectura de un flujo desde el principio o desde una versión.
- Suscripciones a todos los eventos nuevos en orden, con una posición que un proyector puede guardar para reanudar desde ella.
Los event stores dedicados lo ofrecen, y también una tabla relacional con una restricción de unicidad sobre el ID del flujo y la versión, más un outbox o un change feed para las suscripciones.
Modelos de lectura y consistencia eventual
Las proyecciones se ejecutan después de guardar el evento, así que los modelos de lectura van por detrás milisegundos o, durante un atasco, mucho más. Diseña para ello:
- Tras un comando, devuelve la nueva versión y deja que el cliente espere hasta que el modelo de lectura se haya puesto al día, o muestra el resultado a partir de la respuesta del comando.
- Da a cada proyector su propia posición, para que un índice de búsqueda lento no retrase la lista de pedidos.
- Haz que las proyecciones sean idempotentes. Los proyectores se reinician y vuelven a leer eventos; aplicar OrderShipped dos veces no debe contar dos envíos.
La recompensa es que los modelos de lectura son desechables. ¿Una pantalla nueva necesita otra forma? Escribe un proyector y reproduce el historial en él. ¿Un bug en una proyección? Corrígelo, borra el modelo de lectura y reconstrúyelo.
Un ejemplo práctico: un informe nuevo
Un mes después del lanzamiento, finanzas pide los ingresos por categoría de producto y día, incluido el historial. En un sistema CRUD escribirías una migración y un script de backfill, y el historial anterior al cambio podría haberse perdido. Con event sourcing escribes un proyector que escucha OrderPlaced y OrderRefunded, lo apuntas al inicio del log de eventos y dejas que lo reproduzca. Cuando se pone al día, el informe cubre todos los pedidos desde el lanzamiento y a partir de ahí se mantiene actualizado solo.
El diagrama de arquitectura
Contenido del diagrama
- User
- Command side
- Event store
- Event stream
- Projectors
- Read model: order list
- Read model: search index
- Read model: daily totals
- Query side
- PlaceOrder
- append
- subscription
- GET /orders
Cómo leerlo:
- Fila superior, la ruta de escritura: el comando PlaceOrder del usuario va al lado de comandos, que añade eventos al event store. Una suscripción alimenta el flujo de eventos.
- Derecha: los proyectores consumen el flujo y mantienen al día tres modelos de lectura, cada uno en el almacenamiento que mejor se adapta a sus consultas.
- Abajo a la izquierda, la ruta de lectura: las consultas van al lado de consultas, que solo lee modelos de lectura y no contiene reglas de negocio.
- El bloque Markdown muestra un flujo con sus versiones, lo que facilita explicar la comprobación de la versión esperada.
Los costes
- La evolución del esquema no termina nunca. Los eventos se guardan para siempre, así que las versiones antiguas deben seguir siendo legibles. Planifica el upcasting (convertir eventos antiguos a la forma actual al leerlos) desde el primer día.
- Borrar datos es difícil. Las normativas que exigen el borrado chocan con los logs inmutables; deja los datos personales fuera de los eventos o cífralos por persona y borra la clave.
- Los flujos largos tardan en cargarse. Los snapshots del estado del agregado cada pocos cientos de eventos lo resuelven.
- La depuración cambia de lugar. Los bugs aparecen como modelos de lectura incorrectos, y la solución es un replay, que necesita herramientas.
- Las habilidades del equipo. Modelar con eventos es otra forma de pensar. Con un único dominio central con event sourcing basta; el resto del sistema puede seguir siendo CRUD.
Qué poner en el diagrama
- Dos rutas claramente separadas para comandos y consultas.
- El event store como fuente de verdad, con los modelos de lectura dibujados como derivados.
- Cada proyector y su modelo de lectura, para que se vea qué vistas pueden ir con retraso.
- Un flujo de ejemplo, con versiones, para anclar la conversación.
La mecánica de eventos y consumidores se trata en el artículo sobre arquitectura orientada a eventos, y el patrón transactional outbox explica cómo publicar eventos de forma fiable cuando no usas un event store dedicado. El diagrama de arquitectura de Kafka cubre el flujo en sí.
Constrúyelo en Archboard
- Inicia sesión con Google o GitHub y crea un diagrama.
- Añade una Persona y nodos Contenedor del pack Modelo C4 para el lado de comandos, los proyectores y el lado de consultas.
- Añade una Base de datos para el event store y nodos Base de datos para los modelos de lectura, además de Kafka y Redis del pack Datos para el flujo de eventos y los contadores.
- Dibuja la ruta de escritura arriba y la de lectura debajo, con los proyectores alimentando los modelos de lectura desde la derecha.
- Añade un bloque Markdown del pack Básico con un flujo de eventos de ejemplo.
El ejemplo usa 10 nodos de diagrama, dentro del plan Free.
Preguntas frecuentes
¿CQRS y event sourcing tienen que usarse juntos? No. CQRS funciona con una base de datos normal, y el event sourcing puede alimentar un único modelo de lectura. El event sourcing sin ninguna forma de CQRS es raro, porque las consultas sobre flujos reproducidos son lentas.
¿Cuándo no debería usar CQRS? Cuando las lecturas y las escrituras usan la misma forma de datos, algo que ocurre en la mayoría de las pantallas CRUD. Aplícalo a las partes del sistema donde divergen.
¿Cómo se mantienen los modelos de lectura consistentes con el event store? De forma eventual. Los proyectores procesan los eventos en orden y guardan su posición; tras un fallo, continúan desde ella. Los clientes deben tolerar un pequeño retraso.
¿Cómo cambio más adelante la estructura de un evento? Versiona los eventos y convierte las versiones antiguas al leerlas (upcasting). Nunca edites eventos guardados in situ.
¿Qué es un snapshot en event sourcing? Una copia guardada del estado de un agregado en una versión concreta, para que cargarlo signifique leer el snapshot y los eventos posteriores en lugar de todo el flujo.