Publicado: 11 agosto, 2023
Interesante imagen creada por el modelo de generación de imágenes stable diffusion XL al pedirle un diagrama de Event Driven Microservices Architecture.
La intención de este post es brindar mi visión sobre la utilización de una arquitectura de software para el caso de un hipoteco core banking system. La arquitectura de software es una de las tareas más apasionantes que se puede hacer como desarrollador de sistemas y no es posible ni deseable abstraerse del dominio de esta. La intención es transmitir una panorámica de cómo se llevaría a cabo, así que nos enfocaremos en la vista de alto nivel. Después puedes ver un caso práctico en este post: Implementando un Core Banking System con CQRS-ES: De la Teoría a la Práctica.
Crear el core banking system no es tarea fácil, no es algo que se haga todos los días ni algo que se deba tomar a la ligera pues al final vas a estar manejando dinero, algo que se debe manejar con sumo cuidado, parece una labor monumental pero en realidad no es tan complicado como parece.
La herramienta decisiva no es que base de datos eliges, no es el framework que te gusta más, ni siquiera el lenguaje de programación, y es aquí donde muchos suelen cometer el error de inmediatamente enfocarse en lo técnico y no en lo realmente determinante, la arquitectura del sistema.
Pues bien, definamos primero al menos cinco criterios clave para nuestro Core Banking System.
Trazabilidad, en caso de una auditoría debemos ser capaces de justificar cada transacción que ocurra en nuestro sistema.
Rendimiento, nuestros clientes ven las cuentas bancarias y las operaciones, pero nuestros contadores ven las transacciones de una manera muy diferente, pero ambos necesitan un sistema que responda rápido.
Disponibilidad, tenemos la responsabilidad de mantener confiable nuestro sistema, negar un pago con tarjeta porque no pudimos responder a tiempo a una solicitud de autorización sería inaceptable.
Coherencia, debemos estar seguros del saldo de una cuenta antes de hacer cualquier movimiento.
Mantenibilidad, debemos ser capaces de diagnosticar rápidamente los problemas y solucionarlos. No te metas con el dinero de la gente. Además, nuestro código debe probarse por completo y todo el equipo debe poder trabajar sin pisarse.
La elección que hagamos ahora será decisiva para ser capaces de adaptarnos a nuestros clientes y brindarles la mejor experiencia.
Una arquitectura que cumple perfectamente con estos criterios es Event Sourcing (ES) en conjunto con Command Query Responsibility Segregation (CQRS). Vamos a verlo en detalle cómo lo logra.
Nuestro negocio radica en nuestros datos. El dinero en sí mismo es solo información, el saldo de una cuenta es un número en algún lugar de una base de datos al igual que las transacciones que ocurrieron. Pero eso no es suficiente, queremos ser completamente transparentes con nuestros datos, poder justificar todas y cada una de las transacciones que se realizaron en una cuenta. Nuestra arquitectura debe permitir eso por diseño.
Hay un patrón emergente en la arquitectura de software llamado Event Sourcing (ES). El término apareció por primera vez en 2005 de la mano de Martin Fowler, pero sólo empezó a tomar relevancia en la última década. El mantra de ES es que el estado de un sistema está dado por todos los eventos que conducen a ese estado. Esos eventos nunca se pueden modificar una vez emitidos y se almacenan en bases de datos de tipo append-only, que solo deberían permitir agregar más no modificar datos.
Nuestro event source es un almacenamiento de eventos inmutables.
Esto es muy bueno para la trazabilidad, todo lo que ocurre en nuestro sistema está en nuestra base de datos. Pero ahora incluso la consulta más simple se vuelve muy compleja. Obtener un saldo de cuenta, por ejemplo, nos obligaría a iterar a través de nuestro event sourcing para sumar depósitos y retiros. Esto no solo no es trivial de implementar, sino que también se volverá más y más lento a medida que se acumulen los eventos.
Esa es la razón por la que ES se usa en combinación con CQRS. Este patrón maneja lecturas y escrituras en el sistema con dos entidades muy distintas. En esta configuración, ES ofrece un límite muy natural entre ellos. Esta arquitectura suele denominarse CQRS/ES.
Descripción general de una arquitectura CQRS/ES.
El lado de lectura espera eventos del ES y tiene su propia base de datos, de forma que después de cada evento, actualiza su base de datos en consecuencia.
El lado de lectura actualiza su propia representación del sistema después de cada evento.
A esa unidad de lectura con su propia representación del flujo de eventos se le denomina proyección, en el ejemplo anterior BankAccount es una proyección. Este enfoque tiene enormes ventajas cuando se trata de rendimiento y mantenibilidad:
Podemos tener tantas proyecciones como queramos. Por ejemplo, uno para la API del cliente que contiene usuarios, cuentas y operaciones. Otra para la API contable que contiene los movimientos de activos y pasivos. Esas proyecciones están completamente desacopladas ya que la única fuente de verdad son los eventos.
Podemos agregar o corregir fácilmente las proyecciones existentes. Solo tenemos que reproducir todos los eventos usando la nueva proyección y luego cambiar a la nueva base de datos.
No tenemos que usar exclusivamente un solo tipo de base de datos como SQL en todas partes, podemos elegir un almacenamiento diferente para cada proyección, dependiendo de nuestras necesidades. Por ejemplo, si necesitamos realizar una consulta de grafos podemos proyectar los eventos en una base de datos como Neo4j.
Nuestro sistema, como la mayoría, necesitará manejar muchas más lecturas que escrituras. Sin embargo, debemos mantener un tiempo de respuesta corto en cada solicitud, especialmente en el lado de escritura. Si tardamos demasiado en responder a una autorización de transacción con tarjeta, la transacción se rechaza, lo que genera una mala experiencia para nuestros clientes.
Es por eso que el almacén de eventos debe ser asíncrono. De esta manera, el lado de escritura puede simplemente publicar eventos en él y seguir adelante, sin esperar a que todas las proyecciones los manejen. Ejecutando las proyecciones en diferentes máquinas, podemos absorber un gran tráfico con cero impacto en el lado de escritura. Si el lado de lectura se ve abrumado, incluso podemos replicar la misma proyección en más máquinas, todas conectadas al ES.
La arquitectura puede potencialmente manejar un gran tráfico utilizando un almacén de eventos asíncrono y múltiples proyecciones.
Si bien esto es muy bueno para el rendimiento, ahora tenemos un problema con la consistencia en las escrituras.
Tomamos nuestras decisiones en el lado de escritura, en función de un comando dado y el estado actual del sistema. Si este estado está en las proyecciones, es probable que hagamos una lectura sucia y, por lo tanto, tomemos una decisión basada en un estado obsoleto con el subsecuente peligro potencial de dejar inconsistente nuestro estado del sistema.
Para resolver este problema, dividimos la parte de escritura en pequeños componentes con estado llamados agregados, podemos verlos como unidades de computación. Para nuestro ejemplo sería “bank account”. Un agregado maneja comandos y produce eventos en función de su estado interno. En el siguiente ejemplo, el estado es el saldo de la cuenta bancaria.
Un agregado recibe comandos y produce eventos.
El agregado usa su estado para tomar decisiones de negocio. Por ejemplo, puede rechazar un comando de retiro debido a fondos insuficientes.
Un agregado puede rechazar comandos.
Dado que solo hay una instancia de un agregado por cuenta bancaria, garantiza la coherencia. También no necesitamos depender de transacciones ni bloqueos en bases de datos.
Dada la importancia de los agregados, pues prácticamente la lógica de nuestro negocio descansaría en ellos, se pueden buscar soluciones ya existentes para asegurar la solidez al implementarlos, porque reinventar la rueda si alguien más ya lo resolvió? dependiendo del stack tecnológico que hayas decidido usar, seguro habrá al menos una propuesta, por ejemplo Dapr, un runtime de ejecución de micro servicios agnóstico a la nube, ya lo resuelve con un componente llamado "actor", incluso tiene SDKs para los lenguajes más populares como Java, Python, Dot Net, Go, etc.
Nuestro criterio final. La arquitectura CQRS/ES es más difícil de implementar que las tradicionales, pero mejora la capacidad de mantenimiento con el tiempo.
La mayoría de las partes del sistema son funciones puras, lo que significa que son muy sencillas de probar. Un agregado, que es donde reside la mayor parte de nuestra lógica de negocio, toma comandos y produce eventos sin efectos secundarios. La mayoría de las pruebas inyectarán algunos comandos y simplemente verificarán los eventos producidos.
Dado que la mayoría de las partes del sistema son pequeñas y están desacopladas entre sí, varios desarrolladores pueden trabajar fácilmente en diferentes partes sin pisarse.
Las proyecciones son muy tolerantes a los errores, ya que podemos corregirlos retroactivamente. Si hicimos el redondeo incorrecto en una proyección contable, por ejemplo, no necesitamos migrar los datos después de la corrección, solo necesitamos reproducir los eventos nuevamente.
A grandes rasgos así queda finalmente nuestra arquitectura:
Vista general de una arquitectura CQRS/ES para un core banking system
Caso práctico: Implementando un Core Banking System con CQRS-ES: De la Teoría a la Práctica.
Sin embargo, CQRS/ES no es perfecto, hablaré de algunos drawbacks de esta arquitectura en otra entrega. ver aquí