If you are software developer, architect, or manager, without a doubt you must have heard about the benefits of microservices architectures.
There are clearly disadvantages when it comes to microservices architectures, but let’s focus first on these alleged benefits that we’ve all heard about: team autonomy, scalability, increased velocity, etc. When experts talk about these benefits, there is often a hidden assumption that the system will have been decomposed correctly. This is a big assumption.
It turns out that decomposing a system into autonomous services is very hard to do and it’s something that you are very likely to get wrong, especially if it’s your first time doing this sort of thing.
In this post, we’ll explore how a bad system decomposition affects a development team and its ability to deliver features autonomously. Feel free to skip to the tldr section.
If you are using or considering adopting a service-oriented architecture, it’s very likely that you’re doing it because you are trying to scale your organization. That is, there are several teams that will work on the project and you need to split the system in a way that will allow each team to work autonomously. Non-functional concerns like scalability are probably less of a priority. The hope is that the overhead and complexity that splitting your system into multiple services will bring, will be offset by the fact that multiple teams will be able to work and deliver features in parallel. As we’ll see below though, for this to actually happen, it is critical to split the system into the right services. You don’t want to end up in a situation where you have to deal with the complexities and challenges that a distributed architecture brings, without getting any of the benefits that a modular architecture is supposed to give you.
Michael Nygard has a great article in which he explains why splitting a system into entity services is an anti-pattern. In this post I’ll discuss another anti-pattern for splitting microservices: horizontal decomposition.
Let’s take the example of an e-commerce website like Amazon. Consider a system that contains a customer-facing frontend for ordering and a backend/admin part for employees to update the catalog. The non-functional requirements of the website and admin components (load, security, availability) are quite different so it makes sense to split them into autonomous sub-systems. A service is often considered to be an independent deployable unit (not true! more on this below) and each team gets assigned a component of the system. This might look something like this:
At the beginning of the project, each team should be able to make progress more or less independently from each other. Now let’s see what happens when we need to add a new feature to the system.
Say we decide to start selling books, which means that we’ll need to add a new optional ISBN (International Standard Book Number) field to our product items. Let’s see what services will need to change in order for the system to support this feature:
- We need to be able to set the ISBN value for product so it’s clear that both the Admin UI and Backend need to change
- The ISBN field should be displayed on the website, so both the Website UI and Backend also need to change
In other words, all services have to change. If we had other services, like email notification services or search, they would probably also need to change too. It doesn’t matter that the services communicate with each other via APIs. The services are actually tightly coupled, which means that the respective teams are now tightly coupled too. The teams would now need to agree on the API contracts between them, they would need to test the relevant (distributed) flows and, perhaps worst of all, they would need to coordinate their deployments. For example, we can’t upgrade the backend UI and add a new ISBN field if we haven’t upgraded the corresponding backend yet.
The reason we have such a high level of coupling between teams is that we have split our services horizontally, that is, according to technical functionality. This way, most features that we’ll add to the system will require us to make changes to multiple services.
This is not what we want from an architecture. We want an architecture that will allow individual teams to deliver features independently from one another. It should be quite clear that in order to achieve this, we should split our services in a way that is cohesive.
The question is how to do this? At first it seems impossible: on the one hand we have components like the admin and website backends that clearly should be independent from each other and, on the other hand, sometimes we do want these two services to change together.
The truth is that unless you change your definition of what a service is, indeed you will find this impossible to do. One of the biggest misconceptions about services is that a service is an independent deployable unit, i.e., service equals process. With this view, we are defining services according to how components are physically deployed. In our example, since it’s clear that the backend admin runs in its own process/container, we consider it to be a service.
But this definition of a service is wrong. Rather you need to define your services in terms of business capabilities. The deployment aspect of the system doesn’t have to be correlated to how the system has been divided into logical services. For example, a single service might run in different components/processes, and a single component might contain parts of multiple services. Once you start thinking of services in terms of business capabilities rather than deployment units, a whole world of options open.
Going back to our example, we could define an Inventory Service, which will be responsible for everything to do with product attributes. Obviously, we would assign a single team to this service. The question is where this service will be located. The answer is both nowhere and everywhere.
In terms of source code, this service will probably have its own git repository. Yet in terms of deployment, there won’t be an Inventory Service process running. Instead, multiple components of the system will include the functionality provided by the Inventory Service. In other words, the Inventory Service will run and be distributed across many processes and servers. It would look something like this:
Under the new setup, the Inventory Service is responsible for everything to do with product attributes across the whole system. It doesn’t matter if we are talking about adding a new ISBN field to the admin UI or adding a new ISBN field to the website’s backend API – it’s all the Inventory Service’s responsibility.
In this case, the team responsible for the Inventory Service would add the new ISBN field to the Inventory Service. Once the change is done, the CI/CD pipeline team will be responsible for packing the changes into the relevant components and re-deploying them. Something like this:
A few questions you might have:
- What are the Admin UI, Admin Backend and Website UI, Website Backend components? They basically act as containers of services. They are maintained by their own teams and their sole purpose is to coordinate between services. These components are business-logic agnostic.
- How do I implement this? If you are using Java and Spring Boot, then each backend component can be a Spring Boot Application and each (business) service can be a dependency in the form of a jar that the Spring Boot Application includes.
- Aren’t we redeploying all the components anyway? Yes, we are. But that’s being done automatically by the CI/CD pipeline. The important thing is that teams are autonomous now. Adding a field to a product only involves the team responsible for the Inventory Service.
This post is quite high level and doesn’t go into too many details into how to actually implement this. My goal with this post is to bring awareness to how critical is to decompose systems into cohesive components. It’s not enough with deciding to create multiple, small services. Unless you decompose your system the right way you are not going to get the benefits promised by service-oriented architectures or microservices architectures. It will be all pain and no benefit. I haven’t discussed how to actually decompose a system into the right services. This is the hard part and depends on the particulars of each system. Here are the key take-aways of this article:
- Create services based on business capabilities and not technical functionality.
- Service does not equal process. A service might run in multiple processes and a process might contain multiple services.
- You want to have loosely coupled services. Things that change together should be placed in the same service.
- If multiple teams have to coordinate in order to deliver a feature, you haven’t decomposed your system optimally.
- Decomposing a system the right way is hard. This is why many experts recommend starting with a single, monolithic service. No split is better than a bad split.