Yesterday I released a blog entry talking about how to set-up transactions in a microservice approach. As I continue to explore the microservices world, let me take a moment and talk about granularity. What is the right granularity for a microservice? As mentioned, you can embed a full transaction within one microservice but then the service becomes rather a large one. Some refer to them as monolith services. They facilitate the management of a transaction, but are difficult to re-use. At the other extreme of the spectrum, others limit microservices to one step in a functionality, making them easily re-usable, but complicating the running of transactions to the extreme. These are nanoservices.
So, what is the right balance? Well, it’s more an art than a science. Let’s remember the objective of the use of microservices is to increase re-use. This quest is not new, it was already available in object orientation, web services and SOA. And the same debate about granularity took place then.
Let’s cook breakfast
To illustrate the granularity topic, let’s take a process we all know well. Let’s prepare a full breakfast. As you can see in the diagram, making breakfast consists of three steps, preparing the ingredients, cooking them and then serving them. Let’s dig into the cooking part a little more in details. Cooking ingredients include the cooking of the bacon, the eggs, the toasting of the bread and finally the frying of the potatoes. To cook the eggs you need to heat the pan, pour in the mixture, stir the mixture, add salt and pepper and finally remove the eggs.
What is an appropriate granularity if we wanted to establish this process as a set of microservices. If we stick at the “make breakfast” level we have one service, on the other hand, if we go down all the way to the most detailed level, we may end up with 60+ services. Sure, those 60+ services are probably easier to re-use in other processes, but it’s a large number of services to manage and integrate.
Let’s look at things in a pragmatic way. If we take the service “Heat Pan”, that is one that can be re-used quite often. Indeed, you also need to heat the pan for cooking the bacon and frying the potatoes, not even considering other business processes such as making lunch and dinner. But on the other hand, “Toasting Bread”, which consists in setting up the machine, entering the bread in the machine, define the toasting time, toasting and removing the bread, is pretty specific to toasting bread. Indeed, what else can we toast? So, does it make sense to go deeper than the service “Toasting Bread”? I believe it doesn’t.
This demonstrates that, in the analysis of the service granularity, not all services need to be at the same level. It’s all about pragmatically defining an appropriate re-use level.
Look at the most economical approach
Now couldn’t you say the same for “Cooking Eggs”? Three of the five sub services have a lot to do with the fact that the service is related to eggs. These are “Pour Mixture”, “Stir Mixture” and “Remove Eggs”. However the other two are pretty generic. “Heat Pan” and “Add Salt and Pepper” can be used in many processes. And this is where your good judgement comes in. On the one hand I make things more complex by going one level deeper, but on the other I increase my re-use factor drastically. Does one outweighs the other? This is where the art comes in. One of my old professors told me years ago that intelligence consists for 75% in laziness. Here it is the same. How can I minimize the work I have to do by choosing the right services objects to maximize re-use while minimizing complexity?
Microservices characteristics and granularity
Microservices are defined as they are small, independent processes loosely communicating with each other through language-agnostic APIs. So, a microservice needs to contain a set of functionality that stands on its own, that fulfills an objective. A microservice should never be time dependent on another microservice to perform its functionality. This is where the loosely coupling comes in.
If I stick to my breakfast analogy, “Pour Mixture” expects “Heat Pan” to have taken place, but there is no real time constraint between the two. In other words, once the pan is hot it’s time to pour the mix, but the breakfast will be as good when it is done within a matter of seconds or minutes.
On the other hand, if I develop a microservice to update a piece my personal information (let’s call it A) and have that microservice dependent on another microservice (let’s call it B) to get and put the data in the database, I may have an issue. Indeed, microservice A receives my identifier, transfers it to microservice B and wait till B comes back with the existing information. My user experience is dependent on how both A and B run. Although I have no direct interactions with B, I’m dependent on the speed at which it responds. Should I keep A and B as two different microservices?
Integrate related functionality.
One way to address this is by integrating the functionality of A and B in the same microservice. Yes it is now becoming larger and less re-usable. That is correct. But on the other hand, the B functionality will not be affected by other requests for information coming from other services. So, in this case I will have to duplicate the B functionality in every microservice that queries the database.
Think graceful degradation
What if I want to set-up a central service to perform the database queries and updates, in other words, the B functionality? Well, you have to take into account the dependencies between A and B. For example, when the user enters his identifier in A, a request is placed to B and at the same time the user is told “we’re working on it”. If B does not respond within a given timeframe, action is taken. For example, the user is told “it seems to take a long time”, or if indications come back that things are going wrong, an apology message is sent together with a suggestion to try later.
A is not just waiting for B, A is doing activities to demonstrate to the user things are progressing. So, when you develop microservices, think through the functionality you include in each of them and ask yourself following questions:
- When the service is down, recognize the failure, spin up a new parallel instance or do graceful degradation.
- If the service is slow, timeout, either act (spawn a new parallel instance to compensate for extra workload) or report to the user the unavailability and potentially allow him to retry.
- Service responds with unexpected content, is your service capable of recognizing and handling this? Microservices must have a stable and published contract.
- When-ever possible, use caching mechanisms so the service can still respond if the back-end is down.
Balance reuse and practicality
Although this has more to do with error handling than granularity as such, it needs to be in the back of your mind when you look at the granularity of your microservices. Balance re-use with practicality. How much extra functionality do you want to embed in your service to ensure it operates properly? I hope you now understand that defining the right granularity is more an art than a science. I would add it’s a balancing act, and one that most of us are not used to.
The original version of this post was published on the defunct CloudSource blog.