Microservice architecture had gotten much praise in the last few years. Scalability, fault isolation, and simpler codebase are some of the advantages microservices are meant to provide. In this article, I will talk about how not only microservices fail to deliver some of the advantages, but also what kind of overhead they bring to the project.
What is microservice architecture?
Microservice architecture, as opposed to monolithic architecture, breaks the app down into several detached services, each one providing one specific piece of functionality. While in a monolithic app all codebase is written and shipped together as one unit, in a microservice-based architecture different services may be responsible for different parts of the app. For example, authentication, computing AI, processing images, sending messages between users.
This diagram highlights the key difference between a monolithic app and a microservice-based one. In monolithic architecture, the entire app is seen as one entity and uses its internal workings to process requests. In microservice architecture, the app is broken down into separate services, which are developed and deployed separately. Thus, an API gateway is required which will determine the service needed to process a request and forward it to the service instance. The services can also communicate with each other directly without the need of a user, for example, the analytics service can pull data from all other services (shown with red arrows).
The monolithic-vs-microservice architecture debate is not limited to end-user pieces of software. The same paradigms are used in OS design. Windows, macOS, and Linux are said to have monolithic kernels, while QNX, Minix, and Symbian have microkernels. The latter is used in embedded devices, due to their low CPU footprint.
Benefits of microservices
Before going into the disadvantages of microservice architecture, I think its important to understand the advantages the microservices promise to deliver:
- Scalability – since the app consists of many different and independent services, it is easy to scale specific ones on-demand. For example, if you notice that more resources are required for image processing, while the authentication service sits idle, you may divert server power to that service.
- Fault tolerance – in monolithic architecture, if an app fails, it fails in its entirety. With microservices, if a single microservice dies, the other ones are (most of the time) not affected.
- Simplicity – since each microservice is responsible for only a small subset of features, it is believed they are easier to develop.
- Team independence – since microservices are detached, they can be developed by different teams using different languages and frameworks.
These are the main reasons large organizations side with microservices. I will now explain how microservices fail to deliver these features and even bring more complexity into systems.
Not quite scalable
While scalability is an obvious advantage of microservices, it does not always come for free. While microservices are indeed detached from one another, they still require solid infrastructure to communicate with each other. As the app grows, the intricacies of inter-service communication become more and more of an overhead.
Microservices communicate together via HTTP (most common) or an asynchronous protocol such as AMQP (less common). While these protocols are very robust and quite fast, they do not come close to in-process communication of a monolithic app in terms of speed and reliability. In addition, they will require complex network infrastructure setup and that puts additional stress on the servers. At some point, as you scale, adding another service to handle load will yield less and less results. It is even possible, with many services, that adding an additional instance of a service will actually slow the entire system down.
This graph shows that as you scale the number of instances of a service, its marginal performance gains drop, while the infrastructural overhead grows. In a monolithic environment, these losses are minimized since there is less infrastructure involved, minus the occasional load balancer.
Unexpected failure behaviour
In an app with microservice architecture, services may fail independently. Since each is a separate unit, a failure in one does not crash the rest. Or do they?
Even though microservices are detached, they still have dependencies on each other, like classes in a software module. If one stops responding, it may compromise the ability of other services. What use your app is to your users if the authentication service is failing? They would never get so far to work with the rest of the services.
In a monolithic structure, failures may be contained. Since you have direct access to all of the systems, it may be possible to recover from an error in ways unavailable to microservices. And when the app does crash, it is easier to restart it in a new environment since the infrastructure is much simpler.
Microservices are believed to promote clean and readable codebases by splitting the code into different serviced based on function. By intuition, is may seem obvious: less features = simpler code. In practice, however, this is rarely the case.
While the features themselves are isolated, you have to keep in mind that you now have to write all the code for inter-service communication, which is nonexistent in monolithic applications. This effort will be multiplied each time you add a new service to your lineup. Additional constraints also apply to such inter-service connections: they must be faultless and instant, like the in-process communication of a monolithic app. With these conditions in mind, imagine the effort spent on writing these communication layers, and how much could be done with this effort without the constraints.
Another factor contributing to messy codebases of microservice apps is inter-team communication. Since each team is responsible for its own service, it must communicate constantly with other teams to fit all of the services into one big picture. Given that different services may be written in different languages and frameworks it will be hard for other teams to understand the codebase. On top of that, you now need extensive documentation for each of the services so the other teams may use your service, and vice-versa.
In a monolithic app, such layers of complexity are non-existent. Since all teams are working on the same codebase, it will be much easier to maintain standards and keep everyone up-to-date.
This is one of the biggest enemy of microservice architecture. Initially, microservices are separated by their function. Now, after some time and customer feedback, you realize that there are new features and changes to existing ones such that your microservice layout no longer works. You are faced with either a complete rewrite of the system (which lives until next requirement change) or creating umbrella services to combine multiple existing ones to produce such feature. The first choice is not going to be very popular with the management, while the latter choice will drag you into long-term tech debt.
Such problems do not exist in a monolithic structure, where all modules are already available to one another from the start.
I am not advocating for a complete abolishment of microservice architecture. Of course, there are use cases for it, from microkernels in embedded devices to large-scale web platforms written with microservices. But one must carefully evaluate the project, the requirements, and the potential future changes which may or may not be easier to do with microservices. Microservices are not a universal cure and if your codebase is cluttered, or it does not scale well, the problem here is deeper than a mere architectural decision.