wandres.dev
EVENT SOURCING Y CQRS · el estado como log

El coste: versionado, desfase y cuándo no hacerlo

Ninguna arquitectura se juzga por lo que promete sino por lo que cobra, y event sourcing cobra en tres monedas que conviene contar antes de firmar. La primera es complejidad conceptual y operativa: aparecen piezas que antes no existían —almacén de eventos, procesos de proyección, snapshots, reconstrucciones— y con ellas modos de fallo, monitorización y una curva de aprendizaje que todo el equipo debe subir, no solo quien propuso el patrón. La segunda, y la más subestimada, es el versionado de eventos: los hechos son inmutables pero el negocio cambia, y un reductor debe seguir entendiendo dentro de cinco años los eventos que se escribieron hoy, lo que obliga a estrategias explícitas de actualización progresiva, doble escritura o traducción en lectura. La tercera es la consistencia eventual, que traslada al desarrollador de interfaz un desfase real entre lo que el usuario acaba de hacer y lo que la pantalla muestra. Sobre esa base, la lección argumenta con precisión cuándo el patrón no debe aplicarse en el frontend y qué versión ligera de la idea sí merece la pena casi siempre.

⏱ 20 min

Las cuatro lecciones anteriores han presentado un modelo tan coherente que resulta seductor, y esa seducción es peligrosa porque la coherencia de una idea no dice nada sobre su idoneidad para tu problema. Event sourcing es caro, y su coste tiene una forma particularmente traicionera: es bajísimo el primer mes y creciente después, justo el perfil que engaña a los equipos. Al principio todo son ventajas —escribir eventos es fácil, plegar es divertido, la auditoría sale gratis— y las facturas llegan más tarde, cuando el negocio pide cambiar la forma de un evento que ya tiene dos millones de instancias escritas, cuando el desfase entre escritura y lectura produce el primer error que nadie sabe reproducir, o cuando alguien nuevo se incorpora al equipo y descubre que para entender por qué un número vale lo que vale debe entender cinco piezas en lugar de abrir una tabla. Esta lección es el contrapeso deliberado del entusiasmo. Enumera las tres facturas con nombre y apellidos, explica qué estrategias existen para pagarlas, y termina donde más importa para quien construye interfaces: por qué en el frontend el patrón completo casi nunca compensa, dónde está la excepción, y qué parte de la idea sí deberías llevarte a todos tus proyectos aunque no adoptes nada más.

🎯 Al terminar esta lección sabrás
  • Cuantificar la complejidad añadida en piezas, modos de fallo y curva de aprendizaje del equipo.
  • Dominar el problema del versionado de eventos y comparar las estrategias de traducción, doble escritura y actualización progresiva.
  • Anticipar los efectos de la consistencia eventual en la interfaz y las técnicas para hacerla tolerable.
  • Decidir con criterio cuándo no aplicar event sourcing en el frontend y qué versión ligera adoptar en su lugar.

Tres facturas que llegan tarde

Antes de detallarlas, una observación sobre cómo se distribuyen en el tiempo, porque ahí está la trampa. Las tres facturas comparten un rasgo: son diferidas. La complejidad se nota cuando el equipo rota, el versionado cuando el negocio evoluciona y el desfase cuando hay usuarios reales impacientes. Ninguna de las tres duele durante el prototipo, que es justo el momento en que se decide la arquitectura, y esa asincronía entre la decisión y su consecuencia es lo que hace que este patrón se adopte más veces de las que debería y se abandone a mitad de camino, dejando lo peor de ambos mundos.

La primera factura es de complejidad, y se paga en piezas. Donde antes había una tabla y una sentencia, ahora hay un almacén de eventos, uno o varios procesos de proyección, snapshots con su versionado, un mecanismo de reconstrucción y la monitorización de todo ello. Cada pieza trae modos de fallo propios que no existían: una proyección puede quedarse atrás, un proceso puede reprocesar un evento dos veces, una reconstrucción puede tardar horas justo cuando hay prisa. Y trae una carga cognitiva que no se reparte de forma equitativa: quien propuso el patrón lo entiende, y quien se incorpora dentro de ocho meses tiene que aprender un modelo mental completo antes de arreglar un error trivial. El coste real de una arquitectura no es lo que cuesta construirla, sino lo que cuesta que cinco personas la sostengan durante cuatro años.

A esa carga hay que sumarle una forma de coste que rara vez aparece en las estimaciones y que es real: la pérdida de herramientas. Cuando el estado vive en tablas, cualquier persona de la organización puede abrir un cliente de base de datos y mirar. Cuando el estado es un pliegue sobre un registro, mirar exige ejecutar código, y de repente el analista de negocio, el equipo de soporte y el administrador de sistemas se quedan sin acceso directo a una información que antes les resultaba trivial. Recuperar esa capacidad implica construir vistas legibles y herramientas internas que en el modelo clásico venían de fábrica, y ese trabajo, que casi nadie presupuesta, es una parte nada menor de la factura.

La segunda factura es la más subestimada y merece su propia sección: el versionado. La tercera es la consistencia eventual, y es la que más se nota en la interfaz. Si el modelo de lectura se actualiza de forma asíncrona, existe una ventana real —de milisegundos a segundos— en la que el usuario ya ha hecho algo y la pantalla todavía no lo refleja. El usuario pulsa guardar, la lista no cambia, el usuario pulsa otra vez. Esa ventana no es un defecto que se pueda arreglar con más cuidado: es la contrapartida directa de haber separado los modelos, y hay que diseñar para ella con actualizaciones optimistas, con confirmaciones que no dependan de la vista o con lecturas dirigidas al lado de escritura en los casos donde el desfase resulta inaceptable.

flowchart TD
C[coste acumulado] --> M1[mes 1 escribir eventos es facil]
M1 --> M2[mes 6 primera proyeccion desfasada]
M2 --> M3[ano 1 primer cambio de esquema de evento]
M3 --> M4[ano 3 eventos de cinco versiones conviviendo]
style M1 fill:#a6e3a1,color:#11111b
style M4 fill:#f38ba8,color:#11111b

El versionado: hechos inmutables, negocio cambiante

Conviene ser específico sobre cómo se manifiesta ese desfase, porque en abstracto suena inofensivo y en la pantalla no lo es. El caso canónico se llama leer lo que acabas de escribir: el usuario crea un elemento, la interfaz navega a la lista y el elemento no está, porque la proyección aún no lo ha procesado. El usuario concluye que la operación falló y repite. Las mitigaciones existen y todas cuestan algo: reflejar el cambio de forma optimista en el cliente sin esperar a la vista, devolver desde el comando los datos suficientes para pintar el resultado, o dirigir esa lectura concreta al lado de escritura aceptando su lentitud. Ninguna es gratis, y elegir cuál se aplica en cada pantalla es trabajo de diseño de producto tanto como de arquitectura.

Aquí está la tensión central del patrón, y no tiene solución elegante. Los eventos son inmutables por definición, pero el dominio que describen cambia cada trimestre. Un evento que hoy lleva una cantidad y una moneda mañana necesitará un impuesto desglosado; un evento llamado PedidoConfirmado acabará dividiéndose en dos cuando el negocio distinga la confirmación del pago. La consecuencia es dura de asumir: el reductor que escribas hoy tendrá que seguir entendiendo, dentro de cinco años, los eventos que se escribieron antes de que nadie imaginara el cambio. No puedes migrar el registro como migras una tabla, porque reescribir eventos pasados destruye precisamente la propiedad que justificaba todo el montaje.

🔀

Traduccion en lectura

El reductor conoce todas las versiones y adapta las viejas al vuelo. Sencillo al principio, y con el tiempo produce reductores llenos de ramas históricas que nadie se atreve a tocar.

⬆️

Actualizacion progresiva

Una capa dedicada convierte cada evento antiguo a la forma actual antes de que el reductor lo vea. Mantiene el reductor limpio a costa de una cadena de convertidores que hay que probar.

✌️

Doble escritura temporal

Durante la transición se emiten ambas versiones del evento y los consumidores migran a su ritmo. Es lo más seguro y también lo más caro, y exige una fecha de fin real.

🆕

Evento nuevo, no evento editado

Cuando el significado cambia de verdad, no se versiona: se crea un tipo nuevo y el viejo se congela. Es la opción más honesta y la que mejor envejece.

// Los eventos viejos no se reescriben: se traducen antes de plegar
type PagoV1 = { tipo: 'PagoRecibido'; v: 1; total: number }
type PagoV2 = { tipo: 'PagoRecibido'; v: 2; base: number; impuesto: number }

function alDia(e: PagoV1 | PagoV2): PagoV2 {
  if (e.v === 2) return e
  // Suposicion historica documentada: antes el impuesto iba incluido al 21 por ciento
  const base = Math.round((e.total / 1.21) * 100) / 100
  return { tipo: 'PagoRecibido', v: 2, base, impuesto: e.total - base }
}

Ninguna de las cuatro estrategias es correcta en abstracto, y elegir bien depende de una distinción que hay que hacer con cuidado: si el cambio es de forma o de significado. Añadir un campo opcional, renombrar una propiedad o desglosar un importe son cambios de forma, y los resuelve una traducción. Que PedidoConfirmado pase a significar algo distinto porque el negocio reorganizó su proceso no es un cambio de forma sino un concepto nuevo, y traducirlo sería falsificar la historia. En ese caso la única opción honesta es la cuarta tarjeta: se congela el tipo viejo, se crea uno nuevo y el reductor entiende ambos como lo que son, dos hechos diferentes que convivieron en épocas distintas del negocio.

Ese comentario del ejemplo es más importante que el código. Toda traducción de un evento antiguo incorpora una suposición sobre un pasado que ya no se puede verificar, y esa suposición debe quedar escrita y fechada, porque dentro de tres años alguien la leerá intentando entender por qué un informe de 2024 no cuadra. La regla de higiene que salva proyectos es incluir el número de versión en cada evento desde el primer día, cuando aún no hay ninguna versión que distinguir: añadirlo después obliga a tratar como versión uno todo lo que no lo lleve, que es exactamente el tipo de suposición que preferirías no tener que hacer.

⚠️
El esquema flexible no te salva del versionado

Es tentador guardar los eventos como documentos sin esquema y creer que así el problema desaparece. No desaparece: se traslada del almacén al reductor, que ahora debe defenderse de campos ausentes, tipos inesperados y significados que cambiaron sin avisar. La flexibilidad del formato solo cambia dónde estalla el problema y quién lo descubre, normalmente en producción y tarde. El versionado explícito es trabajo que se hace pronto o se sufre después.

Queda una precisión sobre las cuatro estrategias que ahorra discusiones: no se eligen de una vez para siempre, sino evento por evento y cambio por cambio. Un sistema maduro tiene traducciones para media docena de campos añadidos, una cadena de actualización progresiva para dos eventos que cambiaron de forma y un par de tipos congelados que solo existen en la historia antigua. Esa heterogeneidad no es desorden, es la huella de un dominio que evolucionó, y documentarla en un solo lugar legible vale más que cualquier intento de uniformar lo que nació distinto.

Cuándo no hacerlo en el frontend

Con las facturas sobre la mesa, la pregunta práctica se vuelve concreta. Event sourcing es un patrón de persistencia, y sus tres beneficios —auditoría, viaje en el tiempo, analítica retroactiva— son beneficios sobre datos que perduran. El estado de una interfaz, en su mayor parte, no perdura: vive lo que dura una pestaña abierta y se evapora al recargar. Montar un registro inmutable, proyecciones y snapshots para gestionar qué pestaña está activa, si un desplegable está abierto o qué texto lleva escrito un formulario es pagar las tres facturas sin recibir ninguno de los tres beneficios, porque nadie va a auditar un desplegable ni a preguntar dentro de dos años en qué instante se abrió.

Estado efimero

Pestañas activas, desplegables, posición de scroll, borradores en curso. Se evapora al recargar y a nadie le importa. Auditarlo no tiene sentido y proyectarlo tampoco.

🖥️

Estado que ya vive en el servidor

Si la verdad está en el backend y el cliente solo la cachea, el registro que importa pertenece al servidor. Duplicarlo en el navegador añade complejidad sin añadir verdad.

📐

Dominio sin nocion de historia

Si nadie del negocio pregunta jamás cómo llegó algo a estar así, el patrón cobra por conservar una información que no interesa a nadie. Sobrescribir es correcto.

👥

Equipo sin experiencia en desfase

Si nadie ha lidiado antes con lecturas retrasadas, el primer contacto no debería ser en el proyecto donde además hay que aprender versionado y reconstrucción.

Hay señales bastante fiables de que la respuesta es no. Si el estado es efímero y se pierde al recargar sin que a nadie le importe, no. Si el equipo no ha trabajado nunca con desfase entre escritura y lectura, no todavía. Si la única motivación es la elegancia del modelo o el deseo de probarlo, no. Si el dominio no tiene una noción propia de historia —si nadie del negocio pregunta jamás cómo llegó esto a estar así—, no, porque el patrón cobra por conservar una información que a nadie interesa. Y a la inversa, las señales de que sí merece la pena son igual de reconocibles: un dominio donde la trazabilidad es un requisito legal o contractual, un editor colaborativo donde la historia de operaciones es el dato central, un sistema de sincronización que ya necesita un registro ordenado de cambios para funcionar, o cualquier caso donde el negocio ya lleva su propia historia a mano en tablas paralelas porque el modelo se la niega.

Hay una confusión adicional que conviene deshacer, porque genera discusiones estériles. En el frontend ya existe una técnica que se parece mucho a esto y que casi nadie llama event sourcing: despachar acciones a un reductor puro. La diferencia crucial es la persistencia. Un reductor de interfaz pliega una secuencia de acciones que vive en memoria, se descarta al recargar y no pretende ser fuente de verdad de nada; event sourcing pliega una secuencia que se persiste, se conserva para siempre y es la única autoridad del dominio. Comparten la forma —hechos en pasado, función pura, estado derivado— y no comparten ni las obligaciones ni las facturas. Confundirlos lleva a dos errores simétricos: creer que ya haces event sourcing porque usas un reductor, y creer que necesitas toda la maquinaria industrial para gestionar el estado de un formulario.

La versión ligera que casi siempre compensa

Rechazar el patrón completo no obliga a renunciar a su idea central, y hay una gradación de compromisos intermedios que casi ningún equipo explora porque el debate se plantea como todo o nada. La opción más sensata para la mayoría de proyectos es aplicar event sourcing a un único agregado, el que de verdad tiene memoria, y dejar todo lo demás en el modelo clásico de estado actual. Una aplicación puede persistir el estado del usuario, sus preferencias y su catálogo con tablas normales, y llevar un registro inmutable solo del ciclo de vida del pedido, que es donde el negocio pregunta, reclama y audita. El coste se concentra donde está el valor, y el resto del sistema sigue siendo aburrido, que es la mejor cualidad que puede tener el código que no importa.

🪶

Un solo agregado con historia

Registro de eventos únicamente donde el dominio tiene memoria real. El resto del sistema sigue con tablas y actualizaciones. Beneficio focalizado, coste acotado.

🧾

Registro de dominio paralelo

Se sigue guardando el estado actual, pero cada cambio significativo emite además un hecho inmutable con intención de negocio. No hay proyecciones ni reconstrucción, y sí auditoría y analítica.

🧩

Reductores puros en memoria

Acciones en pasado y estado derivado dentro del cliente, sin persistencia. Trae legibilidad, pruebas triviales y viaje en el tiempo durante la depuración, sin ninguna de las facturas.

✂️

CQRS sin event sourcing

Separar modelos de escritura y lectura con un solo almacén y propagación simple. Alivia la tensión de esquema sin comprometerse con la inmutabilidad ni con el versionado.

Esta gradación tiene además una propiedad que la hace especialmente atractiva: es acumulativa y reversible en la práctica. Empezar por emitir hechos bien nombrados no cierra ninguna puerta, y si dentro de un año resulta que ese registro paralelo se ha vuelto la fuente más consultada del sistema, ya tienes la historia necesaria para promoverlo a fuente de verdad sin haber perdido nada por el camino. La dirección contraria, en cambio, no existe: quien empieza sobrescribiendo no puede decidir después que quería la historia. Por eso, cuando hay duda genuina, el compromiso barato de la segunda tarjeta es casi siempre la apuesta con mejor relación entre lo que cuesta y lo que preserva.

La segunda de esas opciones merece un comentario porque es la más infravalorada y la más fácil de vender internamente. Consiste en no cambiar la arquitectura en absoluto y limitarse a emitir, junto a cada operación relevante, un hecho bien nombrado que se anexa a una tabla de solo inserción. No hay proyecciones, no hay reconstrucción, no hay desfase y no hay curva de aprendizaje: el sistema funciona exactamente igual que antes y además acumula una historia semántica que en dos años valdrá su peso en oro. Es un porcentaje pequeño del coste a cambio de una fracción considerable del beneficio, y es lo que la mayoría de los equipos que dicen necesitar event sourcing necesitaban en realidad.

ℹ️
La decision se toma por agregado, no por sistema

El error de encuadre más común es preguntarse si la aplicación debe usar event sourcing, porque esa pregunta no tiene una respuesta buena. La pregunta correcta se hace una vez por cada agregado del dominio: tiene memoria este concepto, alguien preguntará alguna vez cómo llegó a este estado, hay obligación de justificar sus cambios. Casi siempre la respuesta será que no, y para uno o dos conceptos será que sí. Esa granularidad es la que convierte una decisión arriesgada de arquitectura en una decisión reversible de diseño.

Quedate con la inversion, no necesariamente con la maquinaria

La conclusión que de verdad importa de este nivel no es que debas montar un almacén de eventos, porque probablemente no debas. Es que la separación entre hechos e interpretación es una herramienta de pensamiento que se aplica a cualquier escala, y que el aparato completo de event sourcing es solo su versión industrial y cara. Cada vez que en Redux despachas una acción nombrada en pasado y un reductor puro la pliega sobre el estado anterior, estás haciendo event sourcing en memoria y sin persistencia: por eso las herramientas de desarrollo te dejan viajar en el tiempo, no por magia sino porque guardaron los hechos. Cada vez que un useReducer sustituye a media docena de asignaciones sueltas, estás moviendo tu código hacia el mismo eje. La pregunta valiosa que te llevas no es si adoptar el patrón, sino una mucho más aplicable y que puedes hacerte esta misma tarde en cualquier proyecto: qué información estoy destruyendo con esta actualización, y me importará algún día. La mayor parte de las veces la respuesta será que no importa, y entonces sobrescribir es correcto y barato y es lo que debes hacer sin remordimiento. Pero habrá un puñado de lugares —el estado de un pedido, la vida de una suscripción, la traza de un documento compartido— donde la respuesta sea que sí, y para esos lugares no hace falta reescribir el sistema entero: basta con guardar los hechos ahí, en ese rincón concreto, y derivar el resto. La madurez arquitectónica no consiste en aplicar el patrón completo ni en rechazarlo, sino en saber exactamente dónde termina la parte de tu dominio que tiene memoria y empieza la que solo tiene presente.

⚔️ Pon precio al patron antes de comprarlo
  1. Enumera las piezas que tu proyecto ganaría al adoptar event sourcing y asigna a cada una un modo de fallo concreto y quién lo vigilaría.
  2. Coge un evento que diseñarías hoy e imagina tres cambios de negocio plausibles a dos años. Decide para cada uno qué estrategia de versionado usarías y por qué.
  3. Implementa alDia con dos versiones de un evento y documenta por escrito la suposición histórica que introduces al traducir.
  4. Simula un desfase de dos segundos entre escritura y lectura en una pantalla real y observa qué hace un usuario impaciente. Diseña la mitigación.
  5. Recorre el estado de una aplicación tuya y clasifícalo en dos montones: el que tiene memoria y el que solo tiene presente. Justifica la frontera.
  6. Escribe el argumento de tres frases con el que rechazarías event sourcing en tu proyecto actual, y el de tres frases con el que lo defenderías para un único agregado.
  7. Implementa la versión ligera: añade a una operación existente la emisión de un hecho bien nombrado en una tabla de solo inserción, sin cambiar nada más.
  8. Recorre los agregados de tu dominio uno a uno y responde para cada uno las tres preguntas del encuadre. Justifica el único que se llevaría el registro completo.