If your team is working on microservices, some of the crucial recurring questions would be —
How to define your microservices?
What should be the boundaries of these microservices?
How many microservices is too many for your application or company?
A few years ago, our team had a big beautiful monolithic application. It served us well for more than a decade until it became a bottleneck to our progress. That’s when we decided to break the application into smaller modules.
While modernizing our application and its platform, microservices were a natural choice. But we had to be mindful of the number of microservices that we wanted to create.
With too few microservices, the applications might behave as monolithic applications. With too many microservices, the applications will be hard to maintain.
Here are some of the considerations that our team had gone through while transitioning to microservices architecture. Many of these are also lessons learned during the migration process.
If you are working on microservices architecture, this might be helpful to refine your design further.
High Cohesion but Loosely Coupled Code
The service implementation should have high cohesion. The code inside the service should only implement or meet one specific functional need. It should not try to mix more than one business functionality into one microservice.
For example, you will need a customer in your system to create an order. But Order creation service should not implement the customer creation logic. Customer creation functionality should be an independent microservice.
Once the customer is created, you can pass it in a call to the Order creation service.
We will see the advantage of keeping single functionality per microservice in the list of points discussed further in this article.
Along with high cohesion, the service implementation should be loosely coupled with external dependencies. The code or the database should not interact or depend directly on external code or data store wherever feasible.
In the previous order creation example, the Order creation service should not call the Customer creation microservice internally. You should create the customer outside the Order creation service.
The customer id can be provided as an input to the Order creation service.
This will make the Order service not fail when the Customer service is unavailable. You can still create orders for existing customer ids. Also, the Order service is not aware of how the customer is created. If the Customer service changes tomorrow, Order service has zero impact due to it.
Interact With a Minimum Number of Database Table
To keep the service focused on one specific functionality, it should communicate with fewer feasible tables. The focus here is on the tables which contain transactional business data. It does not consider the static lookup tables used by the transactional tables.
That would be ideal if you could depend on just one functional table interaction per service. However, it entirely depends on your database design.
It might so happen that you are still using the table structure from your monolithic application, which was designed years ago.
Hence, do not stress much to reach the one-table integration constraint if that does not come naturally in your current DB structure for the service concerned.
Varying Speed of Functionality Change
If your existing code has functionalities that change with varying speed, then they should be in their independent services. Change here means how frequently does each of the functionalities gets new requirements for themselves.
Let’s take the example of functionality F1, which gets a lot of new requirements in a year. But functionality F2 is relatively stable. F2 rarely needs any modification or enhancement.
Both these features should be deployed as independent microservices in such a case.
This design can help the services to have different build, deploy and run cycles for each of them. It also makes functionality F2 not get inadvertently impacted by changes made in functionality F1 and vice-versa.
Independent Service Lifecycle
If a functional feature can have its independent lifecycle, i.e., code, build, test, and deploy, it needs to be a microservice on its own. It is similar to the previous point above but not the same.
In this case, two features F1 and F2, might have frequent changes or enhancement requests of their own. But just because they change with almost similar frequency does not mean they should go together in a microservice.
It is always better to have an independent lifecycle for the code so that you can modify the service on its own. This will make the delivery of each feature independent of the other.
Now you can code new features and deploy them on their own without waiting for other’s implementation stage.
Independent Scaling Requirement
Functionalities have different scaling requirements. Some services need to scale faster as they get a large number of requests at a specific time of the day or quarter. Other services might have steady scaling requirements throughout the year.
If a feature has a different scaling requirement than the rest of the functionalities, it needs to be implemented as an independent microservice. With the deployment of its own, the service can scale independently of others.
For example, website visitors might use Dell’s product view service heavily during holiday periods. But its warranty read service might see a spike after a few days from holiday sales when the products are delivered to customers.
Also, everyone who has an intention to purchase a product views them on the site. But once purchased, not all of them search or verify their warranties online. That means the services have different loads to handle during the same period.
Hence, the product view and warranty read features need to be deployed and scaled independently.
Need For Different Transaction Speed
If the functionalities have different transaction speed requirements, then they need to be implemented by separate microservices. It is even advisable for these services to have independent data stores.
Depending on the query speed and requirement, one service can hog onto the database connections for a long time. If the service shares its code and connection pool with other services, this specific slow-performing service will consume most of the resources.
This design can make the faster-performing services starve and have degraded performance.
We can split the slow-performing code into an independent service with its own build, deploy and run cycle, and a separate datastore.
This approach will help in isolating the slow performance and will not impact other services.
Different Business Group Ownership
Your application might be serving the needs of more than one business group in your organization. In such cases, the functionalities of each group should not be clubbed together in the same codebase.
This criterion is an easy one to identify. It also implicitly helps with other criteria discussed previously in this article. Different business groups can have varying speeds of requirements. Also, their features might have varying scaling demands throughout the year.
By differentiating services by their owner business group, you can avoid conflicting requirements getting into the same code base.
This approach can also help you prevent defects due to accidental impact to one functionality when you are changing code for the other business group.
Imagine the frustration of the business people who had no change requested but now have to deal with a defect in production. It is also easy to miss these defects as no one will test the feature in non-prod due to no change requested for them.
Some Other Considerations to Keep in Mind
Be mindful of the following points while deciding the boundaries of microservices:
Avoid creating too small (nano) services: Nano services will create unnecessary overhead. If two functions always call each other and never get executed independently, consider treating them as one single functionality. You can implement them as one service to reduce overhead.
No second-hand information passing: Multiple services should not give out the same information. Only one service should be the source of the data. Rest all services should interact with the IDs/PKs. If other services need complete information, they should use the IDs to fetch the relevant data from the service that connects to the data source.
Experience of the employees: If your team members are highly experienced with defining microservices, they can determine the service boundary appropriately. So, maybe you would like to have at least one experienced developer who has extensive experience in building microservices.
Structure of your database: If you have a single database serving all your microservices, then it might become a bottleneck for your application. You should preferably also implement a micro DB architecture to support many of the considerations discussed above, such as independent scaling, different transaction speeds, independent service lifecycle, etc.
Final Thoughts
All the points discussed here revolve around the idea of how to define the microservice boundaries accurately. There is no universally defined recommended number. All you need to do is create the right-sized services that can adhere to as many considerations listed above.
However, do not worry too much about perfecting the number of services or size of services from the start.
The requirements will keep changing, and with that, your microservice functionality will change. Eventually, you will also have a better understanding of the functional and non-functional requirements.
Hence, instead of perfecting the system from scratch and spending a lot of time designing, better to have a working system in production. Learn from the production behavior and improve the service structure with your enhanced knowledge.
I would love to learn from your experience on microservices. Please feel free to add your opinion or experience in the comment section.
Subscribe to my free newsletter to get stories delivered directly to your mailbox.
Comments