In the article What are Microservices, Amazon Web Services does a great job of defining Microservices.
Microservices are an architectural and organizational approach to software development where software is composed of small independent services that communicate over well-defined APIs. These services are owned by small, self-contained teams.
Additionally, they do an excellent job of outlining the many strengths of Microservices as contrasted to Monolithic applications.
With a microservices architecture, an application is built as independent components that run each application process as a service. These services communicate via a well-defined interface using lightweight APIs.
They make many excellent points and do a good job of highlighting true strengths of microservice architectures.
However, what AWS doesn’t mention in this article are the downsides. If you were to read only this article you would be led to believe that there are no downsides and that all is rainbows and unicorns in the land of microservices, and you’d be a fool to do anything else.
Yet, when you actually attempt to develop a system as microservices there are some clear downsides which are hard to ignore and its common to find yourself wishing someone would have explained these downsides along with the good.
So here they are, I will outline a couple of the biggest problems with microservices and then I’d like to propose a way to help reduce the effects of these downsides with what I call a Hybrid Microservices Architecture.
Notable Microservice Downsides
- Developer context switching
- Maintenance costs
- Communication costs
Developer Context Switching
Developers frequently need to switch between areas of code in the same application. The feature they may be working on may span multiple, or all, microservices in an application. They may have a bug which is blocking them but appears in a different teams microservice. They may have tight coupling between several microservices, and they need them running in tandem in their development environments.
In all of these cases a developer may need to clone, understand, build and run the code for multiple microservices simultaneously. That set of microservices tends to grow over time as well. Most of the time this is a pretty reasonable thing to do but it’s hard to deny that for each microservice you need to work with in this way more and more cognitive complexity is loaded onto the developer. Eventually there is a tipping point where it can feel “too hard” and the time it takes to get the code to a point where you can work on it feels very slow.
Each Microservice has a cost. Both in actual server and network costs but also in terms of code maintenance. For example, you may have multiple microservices all with a reference to the same library which needs to be updated. That work has to be duplicated across multiple repos, multiple teams.
Each microservice may have its own deployment pipeline, or its own testing infrastructure, its own linting rules, its own repo configuration, its own secrets to rotate.
Even if you utilize shared libraries or tools, when you make a feature improvement for the tool you have to roll out the new version to every independent repository. The patterns and the code are duplicated across multiple repositories, increasing the effort needed to make improvements.
Because microservices are independent processes, they need to communicate with each other through their respective APIs. This communication requires careful specification of inputs and outputs as well as versioning and backward compatible support for other services which may lag behind on their own updates.
In a monolithic application this communication may simple be between two functions in the same code and a simple refactor is all that’s needed. But microservices cannot be monolithically refactored, they need to carefully coordinate their breaking changes and use additive techniques and deprecation schedules.
Additionally, communicating over a network effectively may require special communication patterns such as Queues or Streams to manage throughput and reliability. And further, special considerations may need to be put into place to prevent circular messages. This has a monetary and cognitive complexity cost.
Microservices are typically isolated; they have their own database and services such as Queues, Streams and File Storage will likely be considered an internal implementation detail of the service. That infrastructure will need to be duplicated for each service.
Many times, a service will rely on the data owned by another API, but because those services are isolated, they must not access each other’s internal infrastructure directly. So, they go through the API of the other service to fetch or stream data and duplicate it into their own system. Both the storage and the code needed to do this duplication of data has a non-trivial cost.
Despite all of these downsides the decision tree to choose a microservice architecture vs. a monolithic architecture is still usually pretty clear; yes, you should do microservices, it is worth the cost.
However, I would like to share an additional approach which I am calling a Hybrid Microservice Architecture which I believe can help reduce the costs of developing microservices.
The primary idea of a Hybrid Microservice is to strike a balance between the costs of a microservice and the strengths of a monolith. The strength of the monolith, despite all its problems, is that the code is consolidated together in a single repository.
The second idea of the hybrid architecture is to define the boundary of a microservice not to be based on features or data but rather teams. Each team will consolidate all of the code of their microservices into a single repository.
Yet even though we’re consolidating our code into single repositories per team, I am not suggesting we should simply have multiple smaller monoliths.
Rather, the hybrid approach should adopt a “Mode” pattern which will allow this single repository to run in multiple modes. This will allow us to structure the code into multiple runtime components, which can continue to leverage the strengths of microservices and scale independently based on features and usage.
The mode pattern is simple, rather than having a single entry point, the service declares all possible modes that it can run as, and command line arguments are used to cause the process to run in a single mode. Each process can only run in a single mode.
- Web Server
- Cron Job
- Function or Lambda
- Stream Handler
The advantage of this is that the business logic of the application is shared in the same code base for all modes of the application. The models, the repositories the utilities, etc. You don’t need to an extra repository to create the shared code between them and increase the maintenance and cognitive costs of publishing a library and referencing it across multiple other repositories.
Additionally, since the code is identical between all processes running in the same microservice (just in different modes) you can safely communicate with shared data storage, such as the database. Normally there is a tension between two applications calling the same database directly because their code may be different and altering the schema of data that another application depends on can cause issues with the other applications. When this happens your database becomes an API in and of itself and can become extremely difficult to change safely.
Additionally, facilitating network-based communication between two microservices of the same team can be expensive, feel unnecessary and be slow. Requiring a Cron Job to call into web APIs to simply to get the data it needs to do some work can be very prohibitive, especially when it needs to crunch large amounts of data.
Therefore, the hybrid solution posits this hypothesis:
It’s safe for multiple microservices to access the same database directly provided they have identical code at all times.
And here is the summary in bullet point format:
- Organize into as few of teams as possible
- Each team has a single repository for all of their code
- Each team repository implements all required Modes
- Each microservice owned by the same team runs the same code
- Each microservice runs a single mode
- Each microservice running the same code is safe to access the same internal services
I have made a public, Open Source Hybrid Microservice framework called Grove for use in code or as a proof of concept.