Eventos de Dominio, Event Sourcing y el Modelo de Actores
Imaginemos el hipotético caso de una institución bancaria que debe llevar evidencias probatorias a los estrados de un tribunal y que frente a un reclamo de un cliente debido a inconsistencias en los movimientos de su cuenta, se pueda cotejar categóricamente la realidad histórica de los sucesos traídos a estudio.
¿Cómo podríamos recopilar toda esa información de manera certera y precisa a modo tal que sea totalmente valedera para el juicio? Lo más probable es que el equipo de desarrollo de la institución bancaria haya desarrollado la solución agregando logs de auditoría de manera imperativa. Si bien es una solución valedera en muchos casos, particularmente en éste no nos serviría de evidencia completa y confiable que refleje consistentemente los sucesos sucedidos en el sistema desde el primer día de funcionamiento del mismo. Sería de evidencia incierta y dudosa que no nos permitiría a través de su análisis arrojar certeza. Lo más probable es que durante el juicio estemos en desventaja.
¿Cuál es el problema con los logs de auditoría?
El problema es la pérdida de información. Cuando agregamos logs de manera imperativa a un sistema, depende de nosotros programadores reflejar exactamente el historial, sin pérdida de información, de todas las transacciones de negocio con todos sus datos asociados. Si bien en la teoría suena viable, la agregación de cientos o miles de líneas de logs esparcidas en el código, ubicando a cada una en el lugar exacto no solo agrega complejidad accidental sino que también además de ser una tarea con muy alta probabilidad de introducción de errores, agrega un problema extra del cual estar pendiente y mantener. ¿Qué pasaría si olvidamos tan solo 1 línea de log en el primer deploy a producción? ¿Qué pasaría si en un sistema evolucionado un programador borrase una de éstas líneas? Los logs de auditoría no podrán responder la completitud de los sucesos históricos.
Los logs no son suficientes para reconstruir el estado actual del sistema, son incompletos y por ende ineficaces, por ello no serán válidos para nuestro juicio en el tribunal.
¿Cómo probar categóricamente los sucesos históricos?
La respuesta es Event Sourcing. Con Event Sourcing no perdemos información y esto es importantísimo. Event Sourcing no es una técnica nueva que surge con la tecnología, para nada, de hecho es un práctica habitual en la cotidianidad de muchas profesiones desde hace muchísimos años. En el Dominio Contable su uso es natural, nos brinda la posibilidad de hacer heurística partiendo de todas las carpetas contables de una entidad y responder con exactitud a los hechos ocurridos a lo largo del tiempo.
Con Event Sourcing cada intención queda reflejada, es decir, no se alteran ni se borran sucesos ¿Qué significa ésto? Haciendo una analogía con el rubro contractual vemos que en toda relación que se celebra cuando se concurre a una escribanía lo que hace el notario es certificar las firmas que obran en ese contrato, es decir, las partes firman el contrato delante del notario dando fe el escribano de esas firmas y ese acto queda registrado en un libro protocolar del escribano ¿Qué pasa si una de las partes quiere revocar o disolver ese contrato?
¿Acaso borra la firma registrada del contrato, la tacha, o rompe con sus manos el papel fehaciente a la conformidad de las partes? Por supuesto que no. El contrato, por ley, deja en evidencia histórica dicha conformidad. La disolución de un contrato se lleva a cabo libremente mediante la instrumentación de un escrito en que ambas personas con plena conformidad resuelven disolver esa relación contractual estampando sus firmas. Como vemos se añade otro documento para describir la anulación del primer documento. La evidencia histórica queda registrada.
Con Event Sourcing sí tendríamos la completitud de los sucesos que exige el tribunal para que éste pueda emitir un juicio ético de lo acontecido.
¿Es caro hacer Event Sourcing?
Si guardamos miles o millones de eventos de dominio almacenados en ser varios terabytes por supuesto que el costo de almacenamiento será bastante más caro que simplemente guardar el estado actual de todas las entidades de un sistema.
Pero nos tendríamos que volver a preguntar lo anterior haciendo un poco de filosofía: ¿Cuál sería el costo de no contar con la posibilidad de analizar la información histórica frente al tribunal? Seguramente mucho mayor. Y ese costo puede incurrir en muchas situaciones futuras más allá de este caso puntual del cliente enojado debido a las sospechas respecto de los movimientos en su cuenta. Citando otro ejemplo en un contexto de comercio electrónico: ¿Qué pasaría si decidiésemos no usar Event Sourcing y por ello no supiéramos que gran parte de los clientes quita ítems de su carrito antes de proceder al pago? Hay un costo de oportunidad asociado. Si no registramos la historia no la conocemos. Con Event Sourcing podemos tener ésta información, podemos tomar decisiones estratégicas apoyándonos en los sucesos sucedidos, que si bien incurren en costos de almacenamiento brindan un valor de negocio esencial. El Retorno de la Inversión (ROI) de aplicar Event Sourcing es inversamente proporcional al esfuerzo realizado que traiga a estudio conclusiones heurísticas que respondan cualquier suceso sucedido.
Event Sourcing es poder reconstituir una entidad a partir de sus sucesos históricos.
Siguiendo el ejemplo del tribunal, sería poder reconstituir a la fecha presente, la cuenta de cliente enojado partiendo de todos los movimientos ocurridos desde su apertura.
Como somos programadores y adherimos fervorosamente a Domain Driven Design (DDD) vamos a replantear lo que es Event Sourcing utilizando un lenguaje más técnico:
Event Sourcing es poder reconstituir un Aggregate a partir de sus Eventos de Dominio.
Dentro de un Bounded Context, un Aggregate recibe de manera imperativa un comando que representa una acción determinada, y dicho Aggregate como resultado de realizar esa acción, emite un evento de dominio que luego es persistido. Frente a N comandos, tendremos N eventos de dominio que componen todo el ciclo de vida del Aggregate:
Tal vez como lector se habrá dado cuenta de un valor de negocio sumamente importante al contar con todos los eventos de dominios de todas las entidades de un sistema. Me refiero a las proyecciones, algo que jamás podríamos pensar si nuestro sistema guardase solamente el estado actual de las entidades. ¿Se imaginan la posibilidad de poder reconstituir un Aggregate en una fecha puntual de la historia? ¿Qué tal armar un reporte constituido por determinadas entidades de nuestro sistema en una fecha precisa y a una hora en particular? Las posibilidades son inmensas. Y ésto lo podemos hacer porque Event Sourcing es determinista y nos asegura la completitud certera de los hechos.
Otro uso de la fuente de verdad histórica representada por los eventos persistidos en nuestro sistema, es la posibilidad de notificar cada uno de éstos eventos a otros Bounded Contexts o realizar notificaciones dentro del mismo Bounded Context.
Akka al rescate
Implementar Event Sourcing no es una tarea trivial, por ello necesitamos de una herramienta madura que nos haga poner foco solamente en cuestiones de nuestro dominio, dejando de lado todo lo que concierne al dominio computacional de persistir y leer eventos. Akka nos provee de la caja una API que nos permite, entre varias cosas, añadir eventos a una base de datos y recuperar eventos.
Una de las grandes ventajas de Event Sourcing es que los eventos inmutables se van añadiendo a la base de datos (nada muta de estado), lo que permite un ratio muy alto de transacciones que desde luego performa mucho mejor que hacer un Update sobre una base de datos relacional.
Akka persistence también nos brinda la posibilidad de que un actor al persistir su estado, éste pueda volver a ser recuperado en caso de que haya sufrido un restart a causa de un crash de la JVM.
Veamos algo de código de un caso concreto
En TiendaNube usamos Event Sourcing. Cuando comenzamos a repensar el Subdominio de Catálogo identificamos los objetos que refieren a los diferentes árboles de categorías de cada una de las tiendas. Frente a ello vimos la necesidad de plantear, dentro del Bounded Context Catálogo, una abstracción efectiva que modele justamente el árbol de categorías. Algunos de sus casos de uso son:
- Agregar una categoría
- Modificar una categoría
- Eliminar una categoría
A continuación se muestra una versión muy simplificada del modelo, acotado a los casos de uso previamente mencionados.
Éste modelo implementa la lógica de negocio para la resolución de los casos de uso (observe el protocolo definido en las líneas 3, 6 y 9). No hay absolutamente nada que refiera al Modelo de Actores ni a Event Sourcing.
Vamos a ver a continuación el Actor que hace de carcasa al modelo previo dotándolo de todas las virtudes provistas por el modelo de actores:
Vuelvo a repetir: al Actor es solo una carcasa del modelo que nos brinda gratuitamente un límite transaccional explícito y nos libra de lidiar con condiciones de carrera.
La línea 14, especifica cuál es el ID del actor que modela el CategoryTree. En éste caso usamos como ID, el ID del Store o tienda virtual dado que una tienda virtual tiene un solo árbol de categorías.
Fíjese que en la línea 12, declaramos una única variable de instancia que refiere a CategoryTreeState. El estado de ésta variable irá mutando a medida que el actor procesa comandos y emite eventos.
Como el código es similar para cada de uso voy a explicar el código del caso: Agregar una categoría. Recuerde que un Aggregate recibe un comando, acciona frente al mismo y emite un evento. En nuestro caso de uso el Aggregate CategoryTree recibirá el comando AddCategory y emitirá el evento CategoryAdded.
El código anterior modela el comando (línea 1) y su respectivo evento (2). Tanto Comandos como Eventos son Value Objects
El método anterior especifica el protocolo del actor, es decir, que comandos sabrá manejar. Para ello usamos funciones parciales. Cualquier otro comando que enviemos que no se encuentre definido por el dominio de éstas funciones parciales será ignorado por el actor. Fíjese que el protocolo del actor CategoryTree se corresponde 1 a 1 con el protocolo de nuestro modelo CategoryTreeState. Aquí, cuando el actor recibe el Comando AddCategory, se persiste el Evento CategoryAdded que manifiesta la acción de agregar una nueva categoría al árbol de categorías. El método persist recibe como argumento un handler que se ejecuta una vez persistido el evento, y es en éste handler donde realizamos la transición del estado del actor para reflejar el nuevo estado que contenga el resultado de la ejecución de dicha transacción:
Para causar esta transición note que el Actor simplemente delega a su modelo (que contiene la lógica de dominio).
Hasta ahora describimos la secuencia de ejecución desde el momento en que un Actor en memoria recibe un Comando hasta que lo persiste. Lo que vamos a ver a continuación es dado un Comando AddCategory, como se trae a memoria un actor y se restaura su estado a partir de su flujo de eventos:
Akka nos provee los eventos asociados a un Actor específico y nosotros en el método receiveRecover definimos las funciones parciales que sepan interpretar esos eventos y accionar frente a cada uno para iterativemente transicionar el estado del actor hasta componer el estado actual.
Seguramente se éste preguntando ¿qué pasa si tenemos miles o millones de eventos? La estrategia es cada N eventos guardar un Snapshot del Aggregate, una foto, a modo tal que cuando se haga un receiveRecover del Actor, Akka nos facilite los eventos a partir del último Snapshot.
Para seguir viendo
https://leanpub.com/esversioning
https://www.infoq.com/presentations/microservices-events-first-design/
https://doc.akka.io/docs/akka/current/persistence.html#introduction