When implementing changes to a monolithic architecture, you can uncover unexpected behaviors. For instance, modifying a portion of code in one class can cause another to fail. The number and intensity of these unexpected behaviors will depend on how well the monolith was created in the first place.
While maintaining a monolithic architecture you should not only be concerned with performance but you should also be concerned with the monolith’s ability to scale up or down. In addition, training staff on how a monolith is architected and where to make changes properly takes time, slowing down the team’s velocity in order to build new features. Even if the team understands the complete application and can implement a minor fix without affecting anything else, an entire deployment is required and can delay deployment times.
Approaches for Moving to Microservices
Moving to microservices provides faster scalability, as having more minor, smaller services means they will be quicker to start. Team productivity will increase as a result of limiting the context of each service to a more narrow set of business rules. There could be an improvement in performance; however, this also relies on how well the services are implemented (if you have poor code, you will have poor performance). Finally, a hidden advantage is that you are not limited in the languages or technologies that you can use. For example, let’s say you have a service in .NET and another in Python. If both use JSON to return information, then they can communicate with each other or with the front ends.
There are a number of approaches that can be used to transform a monolith to microservices:
The big bang release – Getting rid of the monolith and starting from scratch.
Splitting the development – This means that there will be new microservices living together with the existing monolith. The idea is to work on actively developed applications so everything that is a new feature, as long as it can be accomplished technically and move the required dependencies, will be extracted to a new microservice.
API proxy or substitute layer – Creating a middle layer that will receive all the calls. This layer can aggregate information from multiple services before responding, or even just pass through the request but log some details.
Divide and conquer approach – Used when combining “macroservices” with “miniservices.” Conceptually, they are two different services; macroservices can be as big as a monolith, while miniservices are focused on a single feature or goal.
Strategies to Handle the Challenges Along the Way
There are some challenges that may be encountered during the transformation process, but there are strategies that can help to overcome these obstacles.
Lack of Training
The first issue that can come up occurs when there is not enough proper training, either because the business did not provide a properly analyzed business model or the developers did not follow the proper rules when working with a decoupled approach. If, in the end, you maintain the same database (and perhaps add additional Docker containers), you will still have the same bottleneck. In this scenario, splitting the work and moving new functionality to new services will allow you to slowly move the monolith to a new architecture. While this is a solid approach, it comes with a possible drawback. The team may begin creating several small services without the proper documentation instructing others on where to find functionality that they need in the sea of services that have been created.
Lack of Tests
Another one of the challenges can be interesting and exhausting at the same time; there are no unit tests, no integration tests or any performance metrics. In this situation, you would have a difficult time measuring whether any improvements are being made or if you are getting the same results. A solution to this problem is to create the substitute layer (or Proxy). The main purpose of this Proxy layer is to log results and performance metrics. Once there is enough information, you can start moving the microservices and replacing the calls within the substitute layer from the monolith to the new microservices. This also works as aggregator services, where information that is retrieved from multiple services can be combined, whereas there was only one call before.
No Business Commitment
An additional challenge is when there is no business commitment. This can be obvious when there is resistance to change, and the most important thing is to prove that there is progress as fast as possible. In these scenarios, you will be evaluated on how quickly you can provide something of value to the business. You can again split the development and apply the “divide and conquer” approach. Whenever there are minor fixes like sending an email or creating a PDF based on a template, these are ideal scenarios where the strategy will show its results since there are key functionalities that are easy to start separating. Dealing with resistance to change also requires a few more steps: scoping out the required work as accurately as possible, making the services as self-contained as they can be and juggling with the number of resources you have available, as allocating time to the developers can be difficult.
The last category of challenges is when you are working with a complete rewrite (which could be seen instead as an opportunity) and working with scheduled releases on critical operations. Oftentimes, the “big bang releases” can be prevalent. This happens when there is a dedicated team and no new features are included in the application, as the main objective is to prepare for a future where change may occur and the work will start to increase.
Techniques for Success
Below is an outline of solutions and implementations to create new services in a more productive way.
Creating miniservices – These are used to accomplish generic tasks such as sending emails, uploading files to a file repository like S3, generating PDFs based on a template or validating XML with XSLT files.
Creating NPM packages for your organization to generate a bootstrap of code – For example, create-react-app is something used a lot in the industry, but at First Factory we have created a CLI that starts a new project with create-react-app but also configures the React Router, installs some of the shared libraries and configurations and integrates with a Cognito layer that we use for internal authentication. Every time we need to create a new application that relies on the infrastructure that we have defined, it removes all the repetitive work. In other organizations, similar projects can be created where the stack and the installed libraries may vary, but overall the process is to reduce the time needed to create a new service.
Create a process to migrate data automatically – Our Director of Engineering created a base ETL that relies on code handlers, so whenever we need to do a data migration we analyze the data sources and, for each data source, a handler is generated that extracts the information in a particular order, transforms it and then loads that into a database. Then, the only objective of the team is to create the handlers that will deal with the custom logic for each project, but this base template manages the overall wrapper.
A microservices architecture provides the greatest flexibility and resiliency for modern applications, allowing for high scalability and quicker delivery of new functionality. The key to success when migrating from a monolithic to microservice-based architecture comes down to the traditional core foundations of software development: due diligence in planning, risk mitigation and having the focus and patience to follow a comprehensive plan closely.