MicroService is a hot word in the industry today. Almost everyone wants to migrate their systems to a MicroService based architectures so that they can do wonders with it. But, before we jump in, we should pause to think. Are we just following the crowd? Will this help my product? And most of all, are we really moving to MicroServices - or just messing up a working enterprise?
All the hype behind the MicroService is well justified. But it is meaningful only if we can truly understand the essence and apply it to our architecture - not just the syntax. Deploying on the cloud, breaking into small parts or just using Spring Boot and Dockers do not make a MicroService architecture.
Then, what makes a good MicroService architecture? To understand this, one must understand the core principles of a MicroService architecture. One must understand why MicroService were introduced and what problem we are trying to solve with the MicroServices. Then we can introspect to understand if our architecture really addresses these problems.
It is easy to say that we must design our microservices well. But what exactly is a good design? How do I know if my design is good? Here are some key points that we can consider while evaluating our design.
MicroServices were introduced to enable fast & flexible change. In a true MicroService based architecture, we should be able to update and replace any service with an enhanced version - with no impact on other services; and any change-user-story should impact just one MicroService. Are we getting there?
In order to achieve this the architecture should address the following principles:
A MicroService should implement a single business function. Now, "a single business function" is a hazy phrase. For an outsider, producing a movie is a single business function. For an insider, it is a massive endeavor involving a variety of different tasks.
Any business function is quite similar. So how do we decide if the "single business function" is right in its demand for a single dedicated MicroService?
This is a common design problem when we work on an enterprise application - where we have a wide variety of functions. Any function wants the others to come in a black box. But, each is too big to fit into a single MicroService.
A typical solution for such a problem is a tree-leaf structure - where one MicroService works as the gateway to a chunk of MicroServices, which may split further. This modularity should continue to split till a single line of code. In a well designed code any single line of code should hold a unique aspect of the functionality.
Now, we have another problem! Certainly, we cannot go on splitting our MicroServices to that extent. When do we stop splitting MicroServices?
Ideally, a MicroService should define a functionality that can change independent of others. If we notice a couple of MicroServices that always change together, we should know that we have messed up.
And if we notice a MicroService changes for almost every change; and it changes by changing only a small part of its code - we have messed up again.
If a MicroService runs into a problem, it should realize this quickly and it should fail gracefully - without toppling the system. If our problems cascade along several MicroServices, it means there is a problem in our design.
Ideally, our MicroServices should be laid out in such a way that a service can contain a problem within itself. But that does not mean that a MicroService tries to cover up its problems. If it is not able to do its job, it fails. And it fails fast. It fails as a service. And, the rest of the system should be able to respect the problem.
Here, a problem is not an exception, it is just another condition that our code handles by design. Now, if the MicroService handles a lot of functionality, it will have many reasons to fail. And a failure will impact a lot of functionality that it should not.
On the other hand, if the MicroServices are too thin, we will have many of them failing for a single problem. In either case - we have messed it up.
Like most of the design concepts, this again is a hazy phrase. How loose is "loose"? How do we know that we are loose enough? In the true sense, loosely coupled services should be communicate on discovery rather than invocation. Ideally, a loosely coupled pair of MicroServices should not share a single line of code.
This does not happen in real life. If two MicroServices are developed in Spring Boot, they will naturally share the Spring jars. That is fine, because neither MicroService expects the other to use it. By design, each MicroService should be independent in code.
Not sharing code does not mean duplication of code between MicroServices. The point is, two MicroServices should not need the same code. If we notice duplication of code, we should know our design is wrong. In its true sense, Loose coupling means coupling based on functionality and standards - rather than a mutually agreed interface. Hence, we can see typical MicroServices using the REST API to communicate. They communicate using discovery based on functionality rather than direct invocation.
Such "Loose" coupling comes at a cost - the cost of translating between implementation specific data structures to standard external API. Having too many MicroServices makes it impossible to have such loose coupling. Even if we can, that leaves a lot of our code busy with this translation rather than working on the real functionality.
And of course, if we have very few of them, we have a messy bunch of monoliths - which defeats the purpose. Hence, it is very important to identify an optimal set of micro services that can cooperate independently.
This is not limited to MicroServices. Abstraction is perhaps the most important aspect of any design. Whatever architecture paradigm we may use, abstraction is essential in a good design.
What exactly is abstraction? As per the abstraction principle, any unit of code should do its job without exporting any dependency on how it does the job.
What does that mean? Consider the simple System.out.println(). Did we ever need to look into how it works? We know what it does not how. Any well designed block of code should export the "what" and not "how".
If we need to look into how the functionality is implemented, it means the functionality is not implemented well. This gets more and more important as the size of our modules increases. As the size of modules increases, the gap between what and how increases as well.
If this does not happen, if one MicroService depends upon how another MicroService does its job, it means the MicroService is not abstracting a functionality. It means they are not independent and the design is messed up.
In a large enterprise, each service evolves at a different rate. If the system is well designed, most changes limit to very few services. Because of this, change in one service should not require us to touch others. This is possible only if we have an independent CI/CD for each MicroService.
Similarly, each MicroService has its own load patterns. As the enterprise scales, some services retain the same load, while others grow very fast. If we club them together, it would lead to a redundant capacity for some services or starvation for the others.