Microservices, or Microservice Architecture (MSA) is an architectural style that structures an application as a group of services that are maintainable, testable, loosely coupled, and can be deployed indenpendently. They must do “one thing” and do it well. Great!, but how do we achieve the previous statement? Let’s focus on a microservice (service) itself.
Due the fact that microservices must be able to deploy themselves indenpendently we can treat them as (actually they are) standalone applications. We can take advantage of it by applying the same architecture you would to a monolithic software. This way you can apply your SOLID principles, your favourite architecture and strategies you prefer, separation of concerns, etc.
In this post I bring you my approach to a SOLID NodeJs application written in TypeScript. Its goal is to manage the user accounts of an application. It performs operations such as Sign-in, Sign-up and session token refresh. It implements a rich way of horizontal and vertical separation and very well defined boundaries. Horizontal and vertical separation of concerns are very important for microservices. By achieving a well defined vertical separation we isolate functionalities from each other making them easy to move to a another service once you decide your service has grown too much. By achieving a well defined horizontal separation we isolate layers from each other making it really easy to move them to a different service once you decide to share it with other services or make a dedicated one for its own.
The service is devided into 4 funcional layers:
- Main: Service start point.
- API: Listen to requests.
- Business: Brain of the application.
- Data: Data handling.
Database and Gateway layers are actually servers and are out of the scope of this post. The direction of the arrows means dependency or visibility where business layer is at the top level of abstraction.
This layer holds the start point of the service and performs common microservice’s tasks like requesting configuration to the config server, registering itself in the discovery service, starting the REST server in order to listen to incoming requests, connect to a database (if any) and any other possible task it may require for startup. This layer also houses the dependency injector.
Request controllers, data validators, validation models (usually own layer models), and model mappers are located here. It’s goal is to receive requests, validate their content and forward it to the business layer. The API layer communicates with the business layer through the use-cases objects injected in the controllers.
This is the core of the application. Its goal is to interconnect the application’s extremities, perform the logic and despatch results to its respective destination. Data flows through RxJS pipes. Layer’s models and data repositories interfaces are also placed here.
This layer will handle the data extracted from the source and sends it back to the upper layer. Business layer communicates with data thought its interfaces abstracting it from data origin. Layer models such as entities or schema (in case you are using MongoDB) and entity mappers are placed here. It also holds the interfaces to communicate with data persistence, data caching and data cloud frameworks.
Vertical separation of concerns
By separating each operation’s flow with well defined boundaries we isolate each functionality from the rest. By doing this we will be able to move any specific operation to a different service with the least effort once you decide your service has grown too much. Maybe in production, login operations take place 10 or maybe 20 times more than register ones and the load balancer is instantiating tons of user-manager service but only a very few instances handle login requests.
Horizontal separation of concerns
By splitting your application in separated layers with well designed boundaries you isolate those layers making them independendent from each other. This independency plays a great role in terms of Microservices, it gives you the freedom to move any of those layers to a service apart. Consider the following case: You decide to split your token refresh and token validation functionalities in your user-manager service to their own service. Since token refresh and token validation features perform calls to obtain user entities, data layer can then be wrapped in it’s own service in order to provide user entities to whoever performs the request.
All errors will flow from origin to the caller (mostly the controller). The caller will handle the error and provide a proper response to the client. Errors are also logged by a logger object hosted in the MAIN layer and injected into all error prone components. Besides, if you wish to have error logging centralized, the injected logger’s implementation could also broadcast critical errors through AMQP. Such messages can be taken by a Logger service and place them into a log database. My personal solution is to wrap the critical error message within the last 20 or 30 log messages (at any level: info, debug, warning, etc) and broadcast it in form of stacktrace.
I have published the code on Github in a separated repository in case you want to review it. You wont be able to run it since some parts are hosted in a private package repository, but you can still check the code and see how it’s implemented. Check the code here.
Clean architecture works just as well at any level. The reason is that Clean Architecture doesn’t care how components are deployed. Indeed, a system with a good Clean Architecture doesn’t know which deployment option it’s using. Once your system is abstracted from the details (lower level details: system input output, network communication, etc.) the system doesn’t care how or where the data comes from. Your business logic lives in ignorance, it only knows where to get the data, what to do with it and where to forward it.
“The job of good system architects is to create a structure whereby the components of the system – whether Use-cases, UI components, database components, or what have you – have no idea how they are deployed and how they communicate with the other components in the system”. Robert C. Martin.
- The Clean Architecture
- The Clean Architecture (Book)
- Clean Microservice Architecture
- TypeScript Microservices
- Building Microservices