In microservices-based architecture, applications are built as a suite of services that are managed independently. This translates into an advantage over monolithic applications in the sense that they are easier to maintain and scale.
I am introducing in this post my own approach to a bank application, with a microservices-based architecture and using the MERN stack, which can manage accounts, customers, and loans, but also make predictions of potential loan defaults and store data on-chain.
In this article
Overview
The purpose of this dummy bank application is to develop a user interface that allows employees to create, update, and delete accounts, customers and loans, store loans on-chain and make predictions about potential loan defaults.
Therefore, this application is made up of multiple services that may or may not communicate with each other to successfully perform an action.
To build the application, I have developed a monorepository using Turborepo and Pnpm workspaces. Each microservice has been dockerized so that each service lives in its own container.
The folders packages and services gather all the workspaces.
In packages we find npm packages that contain code and configuration files that are used by other packages or services, and services gathers the set of microservices that make up the bulk of the application.
Services accounts, customers, loans , loans-model and requests start a web server application, client starts a Next.js application and data derived from a TheGraph node are stored ingraph-node.
In addition, two services, customers and loans, include a Hardhat and Foundry environment for developing, testing, and deploying contracts.
Lastly, the back-end and front-end applications are implemented based on a hexagonal architecture.
Microservices
The microservices included in this application are a collection of granular services that mostly work independently although some of them have some dependency on other services.
For example, services customers and loans will start working satisfactorily when the TheGraph node is active in order to be able to deploy their own subgraphs and thus client can consume the data.
Additionally, all services running an Express.js server depend on access to different MongoDB databases that reside in separate Docker containers.
For the rest, only the client will be the one that communicates with the other services to be able to complete their functions.
The services that make up this application and its main characteristics are the following:
accounts: this microservice is in charge of all the actions that are related to the accounts in the bank: create, delete, update and get accounts, deposit to accounts and withdraw from accounts.
client: it is a Next.js application that a user can use to execute the relevant actions with which to interact with the rest of the microservices. You can find more information in user interface.
customers: this service implements all the actions that have to do with bank customers: create, delete, update and get customers.
graph node: data from the PostgresSQL database and from the IPFS network used by TheGraph node will be written in this workspace for its correct operation.
The node configuration along with the configuration of the rest of the Docker images in this application are in the file /docker-compose.yaml.
loans: this microservice takes care of all the actions that are related to loans: create, delete, update and get loans.
loans model: this service implements a pipeline formed by transformer objects and a neural network, which make up a workflow to classify, binarily, loans and thus predict whether or not they may incur in default. You can find more information in classification model.
requests: this service exists to represent hypothetical loan requests from customers, from which to obtain information to interact with other services.
In microservices, therefore, you can find implementations of Express.js web services with access to independent MongoDB databases, as well as Hardhat and Foundry environments for developing smart contracts, subgraphs for indexing data from the events in these contracts, a Flask web service to interact with a binary classification model, and a Next.js application to shape a user interface.
The following sections introduce all of these features by referring to one of the services to attach code as an example.
Back end
Versioned APIs have been developed for four of the microservices implemented in the application, in such a way that persistent data can be created, read, updated and deleted through an Express.js server.
The architecture used for all of them is hexagonal. Next, the main features of the API developed for the loans microservice will be introduced.
File structure
The basis on which the files are structured is as follows:
The server will be instantiated in the file index.ts passing it an object with information about routes, an instance of a WinstonLogger and optionally other configuration data.
Both the implementation of the logger as of the server are in /packages.
services/loans/api/v1/index.ts
When running this script, the server will be available on the port that is passed to it as an argument and that will be assigned in an environment variable file.
In the file scheme that we saw above we see that the API is made up of three folders:
controllers: they are in charge of defining the operations desired by the API of the application. For example, when doing a fetch to the loans server on the route /v1/create with the POST method, the server will assign the following controller to take care of performing the desired actions:
As we can see, in case the body of the request is an instance of the entity Loan, that is, if the request is valid, an interactor will be executed which has already been injected with the loans repository as an abstraction of a data source class, where that repository is implemented.
Entities, as well as other tools that are shared between several services are implemented in the package bank-utils. For example, the entity Loan imported from there can be seen next:
packages/bank-utils/
Below we have a diagram in which the code flow is indicated when executing the controller createLoanController:
Therefore, the result will be obtaining an object of type InteractorResponse, which will give us information about the status of the request as well as messages and data.
In this way, the timely response from the server will be given in each case, as we can see in the implementation of createLoanController in lines 6, 13 and 17.
core: the folder core collects the business logic of each service. It is made up of repositories and interactors. As we saw earlier, the controller will invoke at least one use case, or interactor, that interacts with a repository to get an entity.
repositories: they are interfaces that define all the methods that are necessary to deal with all the use cases of each service.
These methods are implemented in a data source class outside of the core.
For example, we can see part of the LoanRepository below:
interactors: the use cases of the corresponding service are implemented in the interactors.
Controllers use interactors that already have dependencies injected to keep business logic independent of the framework.
For example, we previously saw in controllers that the interactor createLoanWithDep was used, which already has its dependency injected as we can see below:
services/loans/api/v1/core/interactors/index.ts
The implementation of the interactor createLoan can be seen below:
As we can see, this interactor expects an entity LoanRepository which is used to call four of its methods (log, connect, create and close), but the interactor does not depend on its implementation.
data sources: as we already said, the methods that are described in the repository are implemented in a class in the folder data-sources.
We can see below the implementation of the four methods mentioned above and that are used in the interactor createLoan:
In the financial sphere, there are numerous opportunities to make use of the advantages of blockchains. The decentralisation of the data in this technology ensures the security and immutability that is required to be able to save other bureaucratic procedures.
For example, requirements imposed on providers by different regulations result in a long time for the procedures to be expedited and the funds to be provided.
In the event that these procedures could be expedited by securely sharing information between approved credit entities, credit decision time could be substantially reduced.
In this dummy application I have written two very simple contracts in which the necessary methods are implemented to store and update both loans and customers.
Next we can see the functions of the contract LoanManager:
services/loans/contracts/src/LoanManager.sol
Testing the contracts
As already mentioned, there is also an environment of Foundry to be able to test the contracts in a very intuitive and fast way using the language Solidity.
Below we can see part of the test contract to test the functions in the contract LoanManager:
Microservices customers and loans implement subgraphs using TheGraph, which can be deployed to a TheGraph node and thus a client can consume data from it efficiently.
Since the data to be used cannot be public and given the nature of the application, it might be more appropriate to deploy the contracts to a private blockchain than to encrypt the data.
Anyway, I have developed subgraphs using TheGraph -which does not support every single blockchain and network- due to its ease of use and efficiency from the perspective of the application consuming the data.
To create these subgraphs there are three fundamental parts: the manifest, the GraphQL schema and the mappings.
Subgraph manifest
The subgraph manifest specifies which contracts the subgraph indexes, which events to react to, and how event data are mapped to entities that are stored and queried by the TheGraph node.
The keys that appear between braces will be replaced when generating the manifest by values obtained from the contract deployment.
GraphQL schema
Considering the simplicity of the contracts whose events are to be indexed, a relatively dense and interlaced schema has been developed.
To do this, a distinction is made between the creation of entities for:
high-level concepts: concepts such as the entity Loan or LoanManagerContract.
services/loans/subgraph/schema.graphql
low-level concepts: events that emit the contracts.
services/loans/subgraph/schema.graphql
Mappings
Through the functions defined as mappings in the manifest, TheGraph assigns data that keeps obtaining from the events of the contract to entities defined in the schema.
For example, the following function is responsible for writing several entities with the data processed in the store of the TheGraph node after the contract emits the event LoanAdded.
services/loans/src/mappings/LoanManager.ts
Classification model
The microservice loans-model consists of a Flask web service which gives access to a classification model.
We can predict whether a loan may default using this binary classification model after a supervised training using a database of loans from LendingClub.
This is a highly unbalanced data set, therefore the predictions of the minority class have considerably less accuracy than those of the majority class.
No replacement technique has been considered since the only object of the exercise is to be able to make predictions from the user interface.
A Scikit-learn pipeline has been used that integrates the following:
a preprocessing stage with a qualitative variable encoder and a transformer object for variable scaling.
the Skorch wrapper NeuralNetBinaryClassifier for a neural network model implemented in PyTorch.
The neural net Model and the transformer object Encoder can be found in /services/loans-model/utils. The model used is next:
services/loans-model/utils/model.py
We can see the script for the training of this pipeline and its serialization for later deployment below.
services/loans-model/dump_model.py
I have used Skorch since it offers the advantage of wrappping a model implemented with PyTorch and use it with Scikit-learn's GridSearchCV.
In this way, a hyperparameter optimization of both, model and training, evaluated by cross-validation is performed using three folds of the data set.
Here is a snippet from the Jupyter notebook loans_default.ipynb showing this implementation:
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}
Finally, we can see below the code used to execute the Flask app, along with the implementation of the function predict for the route /predict, which is the only route:
services/loans-model/app.py
User interface
A simple web application has been developed with Next.js 13 using the new feature App Router.
It is an application with five routes and four sections, in which some forms and buttons are used to be able to interact with the servers, as well as with the smart contracts.
For example, in every section there is a card for each use case that is implemented in its respective microservice.
Below we can see the functional component in React.js for the creation of a loan:
This is a form using React Hook Form with which the necessary data are obtained to be able to form an entity Loan and pass it to the interactor createLoanWithDep when pressing Submit.
Below we can see the flow chart of this component:
Once again, interactors call a method of a repository to get an entity, the repository in turn is injected into the interactor as an abstraction of a data source class.
File structure
As we can see in the following diagram, this Next.js application consists of the app folder that gathers all the necessary components in the required paths, plus a features folder.
Let's see what these two folders consist of next.
App
The use of the App Router allows us to make use of React Server Components and thus optimize the performance of the application.
In the case of needing to add interactivity on client-side, it is only necessary to place the directive 'use client' at the top of a file.
It is also convenient to warn about the use of conventions for the names of the files. So, page and layout are repeated successively in each route.
A layout is shared across multiple pages, and each page is unique to each route.
In this way, when navigating to the route /loans we will have, among others, the card of the form that we saw above for the creation of a loan:
services/client/src/app/loans/page.tsx
Features
The business logics and data sources for each section (accounts, customers, loans and requests) are implemented in this folder.
The file structure is similar to the one used in the server APIs:
core: in this folder, as we already said, we will find the business logic of each section:
interactors: the interactors are the result of the implementation of all the use cases of each section. They are designed to accept a repository as an argument, and return a function with this repository already injected.
For example, below we can see the interactor createLoan:
The interactor calls the method create from the repository and returns a response of the type InteractorResponse. If an error occurs, an object of the same type is generated with error messages.
repositories: in the repository we define the methods needed to deal with all use cases. Below we see part of the loan repository:
data sources: every use case described in the repositories is implemented in the data sources. Below we see part of the data source that corresponds to loans:
In line 21 we can see a private method that is used for a promise that resolves to an object of type Response.
This is a function that is used by all use cases and that wraps the method fetch to begin the process of obtaining a resource from a server.
Finally, we can see in the following image the loans section and all its use cases:
Conclusion
In this post I have tried to introduce my approach to a simple microservice-based architecture bank application that implements the main use cases for each service.
In addition, an example has been made in which artificial intelligence and blockchain technology coexist, through the deployment of:
a pipeline which includes a binary classification neural network implemented with PyTorch.
Next.js application that gives support to members and customers in Olive Oil Trust, and reduces...
Ready to #buidl?
Are you interested in Web3 or the synergies between blockchain technology, artificial intelligence and zero knowledge?. Then, do not hesitate to contact me by e-mail or on my LinkedIn profile. You can also find me on GitHub.