En la arquitectura basada en microservicios, las aplicaciones se crean como un conjunto de servicios que se administran de forma independiente. Esto se traduce en una ventaja con respecto a las aplicaciones monolíticas en el sentido de que son aplicaciones más fáciles de mantener y escalar.
En esta publicación presento mi propio enfoque de una aplicación bancaria, con una arquitectura basada en microservicios y utilizando el stack MERN, que puede administrar cuentas, clientes y préstamos, pero también hacer predicciones de posibles incumplimientos de préstamos y almacenar datos en cadena.
En este artículo
Perspectiva general
El propósito de esta aplicación bancaria ficticia es desarrollar una interfaz de usuario que permita a los empleados crear, actualizar y eliminar cuentas, clientes y préstamos, almacenar préstamos en cadena y hacer predicciones sobre posibles incumplimientos de préstamos.
Por lo tanto, esta aplicación se compone de múltiples servicios que pueden o no comunicarse entre sí para realizar una acción con éxito.
Para construir la aplicación, he desarrollado un monorepositorio utilizando Turborepo y pnpm workspaces. Cada microservicio se ha dockerizado para que cada servicio viva en su propio contenedor.
Las carpetas packages y services reúnen todos los espacios de trabajo.
En packages podemos encontrar paquetes npm que contienen código y archivos de configuración que son utilizados por otros paquetes o servicios, y services reúne el conjunto de microservicios que conforman la mayor parte de la aplicación.
Los servicios accounts, customers, loans , loans-model y requests inician una aplicación de servidor web, client inicia una aplicación de Next.js y en graph-node se almacenan datos derivados de un nodo de TheGraph.
Además, dos servicios, customers y loans, también incluyen un entorno de Hardhat y Foundry para desarrollar, testear e implementar contratos.
Por último, las aplicaciones de back end y front end se implementan en base a una arquitectura hexagonal.
Microservicios
Los microservicios que incluye esta aplicación son una colección de servicios granulares que mayormente funcionan de manera independiente aunque algunos de ellos tienen una cierta dependencia con otros servicios.
Por ejemplo, los servicios customers y loans empezarán a funcionar satisfactoriamente cuando el nodo de TheGraph esté activo para, de esta manera, poder desplegar sus propios subgrafos y así client pueda consumir los datos.
Además, todos los servicios que ejecutan un servidor de Express.js dependen del acceso a diferentes bases de datos de MongoDB que residen en contenedores de Docker separados.
Por lo demás, solamente el cliente será el que se comunique con los demás servicios para poder completar sus funciones.
Los servicios que componen esta aplicación y sus principales características son los siguientes:
accounts: este microservicio se encarga de todas las acciones que estén relacionadas con las cuentas en el banco: crear, eliminar, actualizar y obtener cuentas, depositar en cuentas y extraer de cuentas.
client: se trata de una aplicación de Next.js que un usuario puede utilizar para ejecutar las acciones pertinentes con las que interactuar con el resto de microservicios. Puede encontrar más información en interfaz de usuario.
customers: en este servicio se implementan todas las acciones que tengan que ver con los clientes del banco: crear, eliminar, actualizar y obtener clientes.
graph node: en este workspace se escribirán los datos de la base de datos de PostgresSQL y de la red IPFS que utiliza el nodo de TheGraph para su correcto funcionamiento.
La configuración del nodo junto con la del resto de las configuraciones de las imágenes de Docker en esta aplicación se encuentran en el archivo /docker-compose.yaml.
loans: este microservicio se encarga de todas las acciones que estén relacionadas con los préstamos: crear, eliminar, actualizar y obtener préstamos.
loans model: en este servicio se implementa un pipeline formado por objetos transformadores y una red neuronal, que conforman un flujo de trabajo para clasificar, de manera binaria, préstamos y asi predecir si pueden o no incurrir en default. Puede encontrar más información en modelo de clasificación.
requests: este servicio existe para representar unas hipotéticas peticiones de préstamos por parte de clientes, a partir de las cuales poder obtener información para interactuar con otros servicios.
En los microservicios, por lo tanto, se pueden encontrar implementaciones de servicios web Express.js con acceso a bases de datos MongoDB independientes, también entornos de Hardhat y Foundry para el desarrollo de contratos inteligentes, subgrafos para la indexación de datos de los eventos en estos contratos, un servicio web Flask para interactuar con un modelo de clasificación binario y una aplicación de Next.js para dar forma a una interfaz de usuario.
En las siguientes secciones se introducen todas estas características haciendo referencia a uno de los servicios para adjuntar código como ejemplo.
Back end
Se han desarrollado APIs versionadas para cuatro de los microservicios implementados en la aplicación, de tal manera que mediante un servidor de Express.js se puedan crear, leer, actualizar y eliminar datos persistentes.
La arquitectura utilizada para todas ellas es hexagonal. A continuación, se introducirán las características principales de la API desarrollada para el microservicio loans.
Estructura de archivos
La base sobre la cuál se estructuran los archivos es la siguiente:
El servidor se instanciará en el archivo index.ts pasándole un objeto con información sobre las rutas, una instancia de un WinstonLogger y de manera opcional otros datos de configuración.
Tanto la implementación del logger como del servidor se encuentran en /packages.
services/loans/api/v1/index.ts
Al ejecutar este script, el servidor estará disponible en el puerto que se le pasa como argumento y que vendrá asignado en un archivo de variables de entorno.
En el esquema de archivos que vimos más arriba vemos que la API se compone de tres carpetas:
controllers: se encargan de definir las operaciones deseadas por la API de la aplicación. Por ejemplo, al hacer un fetch al servidor de loans en la ruta /v1/create con el método POST el servidor asignará el siguiente controlador para que se encargue de realizar las acciones deseadas:
Como podemos ver, en caso de que el body de la petición sea una instancia de la entidad Loan, es decir, si la petición es válida, se ejecutará un interactor al cuál ya se le ha inyectado el repositorio de préstamos como una abstracción de una clase de fuente de datos, donde se implementa ese repositorio.
Las entidades, al igual que otros útiles que son compartidos entre varios servicios se implementan en el paquete bank-utils. Por ejemplo, la entidad Loan importada desde allí se puede ver a continuación:
packages/bank-utils/
Abajo tenemos un diagrama en el cual se indica el flujo de código al ejecutar el controlador createLoanController:
Por lo tanto, el resultado será la obtención de una objeto de tipo InteractorResponse, el cual nos dará información sobre el estado de la petición así como mensajes y datos.
De esta manera, se dará conformidad a la respuesta oportuna por parte del servidor en cada caso, como podemos ver en la implementación de createLoanController en las líneas 6, 13 y 17.
core: la carpeta core, o núcleo, recopila la lógica de negocio de cada servicio. Se compone de repositorios e interactores. Como vimos anteriormente, el controlador invocará al menos un caso de uso, o interactor, que interactúa con un repositorio para obtener una entidad.
repositorios: son interfaces que definen todos los métodos que son necesarios para lidiar con todos los casos de uso de cada servicio.
Estos métodos se implementan en una clase de fuente de datos fuera del núcleo.
Por ejemplo, podemos ver parte del repositorio LoanRepository a continuación:
interactores: en los interactores se implementan los casos de uso del servicio correspondiente.
Los controladores utilizan interactores que ya tienen las dependencias inyectadas para mantener la lógica de negocio independiente de la infraestructura.
Por ejemplo, anteriormente vimos en controladores que se utilizaba el interactor createLoanWithDep, el cual ya tiene inyectada su dependencia como podemos ver abajo:
services/loans/api/v1/core/interactors/index.ts
La implementación del interactor createLoan se puede ver a continuación:
Como podemos ver, este interactor espera una entidad LoanRepository que se utiliza para llamar a cuatro de sus métodos (log, connect, create y close), pero el interactor no depende de su implementación.
data sources: como ya dijimos, los métodos que se describen en el repositorio se implementan en una clase en la carpeta data-sources, o fuentes de dato.
Podemos ver a continuación la implementación de los cuatro métodos citados anteriormente y que se utilizan en el interactor createLoan:
En el ámbito financiero existen numerosas oportunidades donde hacer uso de las ventajas de las cadenas de bloques. Y es que la descentralización de los datos en esta tecnología aseguran la seguridad e inmutabilidad que se requiere para poder ahorrarse otros trámites burocráticos.
Por ejemplo, requisitos que se imponen sobre los prestadores por distintas regulaciones precisan de mucho tiempo para que se agilicen los trámites y los fondos se presten.
En el caso de que estos trámites pudiesen agilizarse compartiendo de forma segura información entre entidades crediticias aprobadas, se podría reducir el tiempo de decisión sobre el crédito de manera sustancial.
En esta aplicación ficticia se han escrito unos contratos muy sencillos en los cuales se implementan los métodos necesarios para almacenar y actualizar tanto préstamos como clientes.
A continuación podemos ver las funciones del contrato LoanManager:
services/loans/contracts/src/LoanManager.sol
Testeando los contratos
Como ya se ha mencionado, también existe un entorno de Foundry para poder testear los contratos de una manera muy intuitiva y rápida utilizando lenguaje Solidity.
A continuación podemos ver parte del contrato de test para testear las funciones del contrato LoanManager:
Los microservicios customers y loans implementan subgrafos utilizando TheGraph, los cuales se pueden desplegar a un nodo de TheGraph y así consumir datos desde un cliente de manera eficiente.
Dado que los datos que se van a utilizar no pueden ser públicos y dada la naturaleza de la aplicación, tal vez sería más apropiado desplegar los contratos a una cadena de bloques privada que el cifrado de los datos.
De todos modos, he desarrollado subgrafos utilizando TheGraph -que no es compatible con todas las cadenas de bloques ni redes- debido a su facilidad de uso y eficiencia desde la perspectiva de la aplicación que consume los datos.
Para crear estos subgrafos existen tres partes fundamentales: el manifiesto, el esquema GraphQL y los mappings.
Manifiesto del subgrafo
El manifiesto del subgrafo especifica qué contratos indexa el subgrafo, a qué eventos reaccionar y cómo se asignan los datos de eventos a entidades que el nodo de TheGraph almacena y permite consultar.
Abajo se muestra el manifiesto para el microservicio de loans:
Las claves que aparecen entre llaves serán reemplazadas a la hora de generar el manifiesto por valores obtenidos del despliegue de los contratos.
Esquema GraphQL
Teniendo en cuenta la simplicidad de los contratos cuyos eventos se desean indexar, se ha elaborado un esquema relativamente denso y entrelazado.
Para ello se distingue entre la creación de entidades para:
conceptos de alto nivel: conceptos como la entidad Loan o LoanManagerContract.
services/loans/subgraph/schema.graphql
conceptos de bajo nivel: eventos que emiten los contratos.
services/loans/subgraph/schema.graphql
Mappings
Mediante las funciones definidas como mappings en el manifiesto, TheGraph asigna datos que va obteniendo de los eventos del contrato a entidades definidas en el esquema.
Por ejemplo, la siguiente función se encarga de escribir varias entidades con los datos procesados en el store del nodo de TheGraph posteriormente a que el contrato emita el evento LoanAdded.
services/loans/src/mappings/LoanManager.ts
Modelo de clasificación
El microservicio loans-model consta de un servicio web Flask el cual da acceso a un modelo de clasificación.
Podemos predecir si un préstamo puede entrar en default mediante este modelo de clasificación binaria tras un entrenamiento supervisado utilizando una base de datos de préstamos de LendingClub.
Se trata de un set de datos altamente desequilibrado, por lo tanto las predicciones de la clase minoritaria tienen una certeza considerablemente menor a los de la mayoritaria.
No se ha considerado ninguna técnica de replacement dado que el único propósito del ejercicio es ser capaces de realizar predicciones desde la interfaz de usuario.
Se utiliza un pipeline de Scikit-learn el cual integra:
una etapa de preprocesamiento con un codificador de variables cualitativas y un objeto transformador para el escalado de variables.
el wrapper de SkorchNeuralNetBinaryClassifier para un modelo de red neuronal implementado en PyTorch.
La red neuronal Model y el objeto transformador Encoder los podemos encontrar en /services/loans-model/utils. El modelo utilizado es el siguiente:
services/loans-model/utils/model.py
Podemos ver el script para el entrenamiento de este pipeline y su serialización para posterior despliegue a continuación:
services/loans-model/dump_model.py
He utilizado Skorch porque ofrece la ventaja de envolver un modelo implementado con PyTorch y usarlo con GridSearchCV de Scikit-learn.
De esta manera, se realiza una optimización de hiperparámetros de modelo y entrenamiento evaluada por validación cruzada utilizando tres divisiones, o folds, del set de datos.
Podemos ver abajo un fragmento del cuaderno de Jupyter loans_default.ipynb en el que se muestra esta implementación:
services/loans-model/loans_default.ipynb
Best mean test score: 0.838, Best std test score: 0.003, Best params: {'classifier__batch_size': 32, 'classifier__lr': 0.01, 'classifier__module__num_units': 32}
Por último, a continuación está el código utilizado para ejecutar la aplicación Flask, junto con la implementación de la función predict para la ruta /predict, que es la única ruta:
services/loans-model/app.py
Interfaz de usuario
Se ha desarrollado una sencilla aplicación web con Next.js 13 utilizando su nueva featureApp Router.
Se trata de una aplicación con cinco rutas y cuatro secciones, en las que simplemente se utilizan unos formularios y unos botones para poder interactuar con los servidores, así como con los contratos inteligentes.
Por ejemplo, en cada sección aparece un tarjeta para cada caso de uso que se implementa en su respectivo microservicio.
A continuación podemos ver el componente funcional en React.js para la creación de un préstamo:
Se trata de un formulario utilzando React Hook Form con el que se obtienen los datos necesarios para poder formar una entidad Loan y pasarla al interactor createLoanWithDep al pulsar Submit.
A continuación podemos ver el diagrama de flujo de este componente:
Una vez más, los interactores llaman a un método de un repositorio para obtener una entidad, el repositorio, a su vez, se inyecta al interactor como una abstracción de una clase de fuente de datos.
Estructura de archivos
Como podemos ver en el siguiente esquema, esta aplicación de Next.js consiste de la carpeta app que incorpora todos los componentes necesarios en las rutas requeridas, además de una carpeta features.
Veamos en que consisten estas dos carpetas a continuación.
App
El uso del router App nos permite hacer uso de los React Server Components y así optimizar el rendimiento de la aplicación.
En el caso de necesitar interactividad en el lado del cliente tan solo es necesario colocar la directiva 'use client' al principio del archivo.
Además es conveniente advertir del uso de convenciones para los nombres de los archivos. Así, page y layout se repiten sucesivamente en cada ruta.
Un layout se comparte en múltiples páginas, y cada page es única a cada ruta.
De esta manera, al navegar a la ruta /loans nos aparecerán, entre otras, la tarjeta del formulario que vimos más arriba para la creación de un préstamo:
services/client/src/app/loans/page.tsx
Features
En esta carpeta se implementan las lógicas de negocio y las fuentes de datos para cada sección (accounts, customers, loans y requests).
La estructura de archivos es similar a la que se utiliza en las APIs de los servidores:
core: en esta carpeta, como ya dijimos, encontraremos la lógica de negocio de cada sección:
interactores: los interactores son el resultado de la implementación de todos los casos de uso de cada sección. Están diseñados para aceptar como argumento un repositorio, y devolver una función con este repositorio ya inyectado.
Por ejemplo, abajo podemos ver el interactor createLoan:
El interactor llama al método create del repositorio y devuelve una respuesta del tipo InteractorResponse. Si se produce algún error, se genera un objeto del mismo tipo con mensajes de error.
repositorios: en el repositorio definimos los métodos necesarios para ocuparse de todos los casos de uso. Abajo vemos parte del repositorio de los préstamos:
data sources: todos los casos de uso que se describen en los repositorios se implementan en los data sources, o fuentes de datos. A continuación vemos una parte del que corresponde a los préstamos:
En la línea 21 podemos ver un método privado que se utiliza para una promesa que resuelve a un objeto de tipo Response.
Se trata de una función que utilizan todos los casos de uso y que envuelve al método fetch para empezar el proceso de obtención de un recurso de un servidor.
Por último, podemos ver en la siguiente imagen la sección de préstamos y todos sus casos de uso:
Conclusión
En esta publicación he intentado introducir mi propuesta de una sencilla aplicación bancaria de arquitectura basada en microservicios que implemente los casos de uso principales para cada servicio.
Además, se ha dejado constancia de un ejemplo en el que coexisten tecnología de inteligencia artificial y de cadena de bloques, mediante el despliegue de:
un pipeline que incluye una red neuronal de clasificación binaria implementada con PyTorch.
unos contratos inteligentes desarrollados en lenguaje Solidity.
unos subgrafos utilizando TheGraph que en la práctica actúan como una capa de indexación entre los contratos y la interfaz de usuario.
Introducción a la obtención y representación de datos de DEXs utilizando TheGraph y React.js
¿Preparado para #buidl?
¿Está interesado en Web3 o en las sinergias entre la tecnología blockchain, la inteligencia artificial y el conocimiento cero?. Entonces, no dude en contactarme por e-mail o en mi perfil de LinkedIn. También me puede encontrar en GitHub.