Transforming Monoliths to Micro-Services – a Structured Gradual Transformation
Micro-Services are the most popular architectural style today - everyone wants to adopt it. New projects can easily do so but existing projects struggle a lot. All businesses have certain obligations to ensure high availability along with upgrading a tech stack. Micro-Services are the key to Business Agility. I would like to share my views on this topic. How can we effectively transform existing legacy Monolithic applications to the latest Micro-Services Architecture?
A monolithic application is one that is a single deployable artifact. A single bundle with all features. Java has a concept of boundary separation using methods, classes, packages, and jars. We know simple concepts that a code within the same class shares the same state (i.e. instance variables) but could be separated by methods.
The methods and state define a class that adheres to the Single responsibility principle ensuring that the object of that class can perform defined actions based on its responsibility. When multiple classes start interacting with each other we create a new boundary to understand their grouping based on packages.
For example, a repositories package will contain classes that help us with the persistence layer.
Code reusability was introduced by these separations but the code was reusable within the same application. Java Archive (JAR) files help us bundle reusable code and share it with other developers. We came up with an abstract concept of modules to ensure code separation by features. The services concept was introduced to separate a code by the execution environment. An executing code (i.e. Service) can interact with other services following some contract.
Module's concept was abstract so far however, from Java-9 onwards it's more constrained now. Without a proper constraint, an abstract concept is not full proof. Why are we discussing these basic known concepts and not about - How to transform Monolithic applications to Micro-Services? It's a pattern that I wanted to highlight to understand the process of transformation.
We humans, understand things using similarities and differences. In the pattern mentioned above at every level, we applied it. For example, methods create a boundary and separate each other which represents differences but what should belong to a method is based on the similarity. A similarity factor that connects the code or logic to be implemented in that method. This concept is applicable at the Class level, Package level, Module-level, and beyond.
In the transformation process, if you have this proper separation till the Packages level, then you have the required groundwork ready. If not yet then please follow the above conceptual understanding and create these boundaries in your monolithic code. Now apply the Java-9 modular constraints in your code and observe the below points:
Define a simple contract through the module boundary using Java interfaces
Encapsulate intricacies completely
Identify required external library dependencies
Identify interdependencies between modules and resolve circular dependencies if any
You will get early feedback when you apply the above constraints using this JDK 9 modular approach. Once you have done these changes, your code is still monolithic but is gradually ready for migration. The next step in the hierarchy is Services. Modules are to be separated by the execution environment. To achieve this, we need to define Java interfaces - similar to how you will interact with a Web Service.
Once the interfaces are ready you should provide a simple implementation class in your respective modules since the code is still monolithic and needs to run on the same JVM.
Now that modules are separated you need to ensure that the database schemas are separated. To achieve this, you can start separating instances of the database for each module so that it will help achieve separation with pure constraints.
The database tables should not have direct referential constraint using a Foreign Key, instead, there should be some loose coupling using some unique identifier as we do in Micro-Services. For example, a Basket Id would be stored in the Order table but without any Foreign Key constraint.
The next and final step would be to extract each module one by one as a separate service and resolve breaking stuff. We can use frameworks like Feign Client which can help in using the interfaces approach to talk to a micro-service. Once the services are separately deployed, I think you have successfully migrated to Micro-Services architecture.
The software industry is evolving very fast but we need to adopt new technologies with some caution. We need to be aware of business limitations and the effort we put into developing a large project over a period of time. Refactoring an existing project and transforming it in a structured way is very important.
Although, one of the disadvantages of the Micro-Services architecture pattern is that the development complexity is increased. A developer cannot directly debug a code and see workflow or code flow across Micro-Service. I was thinking we should preserve the Monolithic modular code during development, but deploy it as a Micro-Service. In doing so, we will get the benefits of both. Just food for thought!