The excessive use of microservices is still widespread, and this is bad for the earth! I assumed it was common knowledge by now, but I was very wrong. This article aims to clearly explain why you should minimize or eliminate the use of microservices and opt for properly structured modular systems (or any better alternative) instead.
Table of Contents
Open Table of Contents
Importance
There’s a Persian proverb that goes:
خشت اول گر نهد معمار کج ― تا ثریا میرود دیوار کج
Which is a shorter form of a poem by Saib Tabrizi:
چون گذارد خشت اول بر زمین معمار کج
گر رساند بر فلک، باشد همان دیوار کج
Meaning:
When the builder lays the first brick crooked on the ground,
Even if he raises it to the sky, the wall will still be crooked.
There’s an equivalent proverb in English:
A good beginning makes a good ending.
Once you open the door to microservices, all the problems that come with them will follow. You suddenly “upgrade” your monolithic software into a distributed system, and God forbid if it’s only for the hype and hearsay! This transition suddenly makes the system more complex, harder to maintain, and harder to debug, and you’re doomed to carry that burden for the rest of the system’s life or your career, whichever ends first. The problem is that microservices are much more hyped than they have actual use. This chart compares some random computer science terms to show their search interest over time, just to give you an idea of the relative hype:
Microservices are a trend, but they don’t deserve to be. The frequency with which a developer encounters the term “microservices” should be as rare as hearing a name like SNOBOL, and yet, here we are, drowning in the microservices hype.
What Are Microservices?
The Definition
Before we start dissecting microservices, it’s essential to clearly establish a common ground. We’ll use the definition provided by microservices.io as our reference point:
What are microservices?
Microservices - also known as the microservice architecture - is an architectural style that structures an application as a collection of services that are:
- Independently deployable
- Loosely coupled
What Microservices Are Not
According to the definition above, microservices are independently deployable and loosely coupled. Based on this definition, anything listed below, as useful as it may be, is not a representation of microservices architecture:
- A monolithic/central database
- One service with more than one responsibility
- Macroservices (large, coarse-grained services)
- Distributed monoliths
The list goes on. If we respect the definition, we should not call these items microservices to ease the communication of ideas. This article specifically targets the pattern that matches the definition.
Microservices — An Example
The following image shows a very, very small example of a microservice architecture:
The orange boxes each represent a container, in which a service is running on top of a potential runtime, on top of an operating system. Connections between the services are not shown in the image.
Modular Monoliths — An Example
This can be an equivalent of the above example in a modular monolith:
This is a proposed modular monolith in its complete form with all the connections. Connections through contracts are facilitated by interfaces that are located in the core module.
Microservices Are the Wrong Answer to Separation
A Terrible Default
Microservice architectures are the ‘new normal’. — Spring
Microservices are a terrible “normal” or “default”. If your only need is separation, microservices are a bad answer to your need due to the number of side effects that come with them.
A Better Answer
A better solution by far is to use modules. Modules have been around forever, and they offer a superior approach to addressing most of the problems microservices aim to solve. Let’s do a 1:1 comparison of microservices and modules. Spoiler alert: the argument favors modules, as there is little (not none!) to be said in support of microservices when pitted against modules!
Microservices vs. Modules
Team Autonomy
Is it really that challenging to instruct a team to work within a specific directory? At the end of the day, a module can simply be a directory within the same project. Once the interface is established, each team can operate independently, just like in microservices, as long as they adhere to the defined bounds. In this regard, there is no difference between modular monoliths and microservices in terms of team autonomy, as clearly defined boundaries are a requirement in both approaches.
Hi, Jake, listen.
All you need to do is to work within theauth
directory. Implement theAuthGateway
interface, and you’re good to go. Meanwhile, Mike’s team can work on thepayment
directory, and Kate’s team can focus on thesync
directory. You have the freedom to do whatever you want within theauth
directory, as long as you implement theAuthGateway
interface correctly and pass the tests.
It’s not that complicated, is it?
Debugging
Debugging a modular monolith is undoubtedly easier than tracing a bug through a network of systems. Good luck identifying a logical bug in a use case that spans 100 microservices — it’s a daunting task that can be a huge waste of time. We don’t live forever, do we?
Fault Isolation
When it comes to fault isolation, microservices have an advantage by preventing a single service from bringing down the whole system in case of a resource leak. This is a valid point; however, it’s not the full story. Depending on the service that goes down, it can still break something or everything due to cascading failures. The other parts of the system might not go down, but they might not be able to serve requests either. Still, microservices have an advantage in this regard.
Note that this doesn’t stop monoliths from reaching the same level of fault tolerance and uptime. The solution is discussed later in the Availability section.
Runtime
There is little debate on the aspect of runtime performance. Modular monoliths introduce negligible overhead, typically just a single function call. In contrast, microservices incur the full runtime cost for each service, from the ground up. This includes both the operating system overhead (whether a Docker image or, worse, a virtual machine) and the language runtime overhead (which can be particularly problematic with virtual machines, especially those like the JVM).
Versioning
In a modular monolith, the entire system is versioned as a single unit, eliminating the need to manage each library’s version separately. This simplification greatly reduces the time spent on versioning, saving many hours that would otherwise be devoted to managing multiple service versions and ensuring compatibility between them. By minimizing manual steps, the risk of human error is also decreased. In contrast, microservices require each service to be versioned independently, allowing for more granular updates and greater flexibility. The problem is, this usually unneeded flexibility is a liability. It introduces significant overhead in maintaining compatibility between different service versions, ensuring consistent communication protocols, and managing multiple deployment pipelines. As a result, the versioning aspect of microservices becomes more labor-intensive compared to a modular monolith, with increased challenges in coordination and a higher risk of errors.
Deployment
One common claim about microservices is that they can be developed, deployed, and scaled independently. A more accurate way to put it is that they must be developed, deployed, and scaled independently. In many real-world scenarios, this “benefit” is a requirement that introduces unnecessary complexity and overhead. Before adopting microservices, you could scale the system as a whole; now, you must inspect every container to determine if any require additional resources. The entire system has become larger and more resource-intensive. Not only do you need to consider the runtime requirements of each container, but you also typically allocate more resources than each service needs. This practice, necessary for a monolith, becomes excessively wasteful with hundreds of microservices.
Now compare the ease of shipping: is it harder to ship a modular monolith or hundreds of interconnected services? The answer is probably clear. The distributed nature of microservices introduces a higher degree of complexity than monolithic deployments. The very definition of microservices is to be independently deployable. Modern tools and practices have significantly simplified the process, but even if deploying each service is as easy as deploying a monolith, there are still N services in a system with a microservices architecture. This translates to increased management overhead, a demand for new tooling, and an additional learning curve.
Understanding the Codebase
When working with a modular monolith, you don’t want to navigate through all directories to understand a specific part of the system. The main difference in terms of understanding the codebase is that instead of knowing the name of the repository or project, you need to know the directory in which the module is located. This is the only major difference when it comes to comprehending the subsystem.
Availability
With a monolithic architecture, your system is either UP
or DOWN
, with no in-between. With microservices, you need
to monitor every service. All the services need to be UP
, and they need to be able to communicate with each other, all
for you to say the system is UP
. If even one out of your 888 services is down, the system can no longer be
called UP
. Consider cascading failures, and it gets even worse to track what actually works and what doesn’t.
The monolithic case is easier to keep track of; however, it may cause a disaster when the system is DOWN
entirely.
This is not a reason to give up on modular monolithic systems altogether, as the solution to that is the use of
failover. In microservices, it’s harder to keep track of failures, but easier
to selectively focus on certain subsystems that are more critical. Arguably, this might not be a convincing reason to
use microservices instead of monoliths, as failovers protect the whole monolithic system from going down.
Modularity / Separation
Separation is probably the most helpful concept in understanding and maintaining a system. However, if one uses microservices solely as a solution for separation, it signals a skill issue. The division unit of software is, and always has been, modules. Software should be separated by modules. If one doesn’t properly use modules to split the code because they can avoid it, that’s another story! Compared to modules, microservices should count as a hack. Think about it. It’s like you don’t like having two chairs in your room, and instead of moving one chair to another room, you build a new house (hopefully in your own neighborhood) with an empty room just to put that chair in it. To put it bluntly, this is precisely what microservices are most of the time. They are a hack to avoid properly modularizing the project or for teams that lack effective communication to do so. So, to separate software that has high-level concerns (which it usually has), modules are most of the time the better solution, not microservices, unless it is not reasonable, which is explained at the end. The key point in both is to properly separate the concerns. If you cannot do it with modules, you won’t be able to do it with microservices either.
Scalability
Vertical Scaling
With monoliths, you scale the system as a whole. With microservices, you scale every single piece. However, why would you want to allocate more resources to one particular part? It’s not like the other parts will eat up the extra resources (unless you have memory leaks, which are not supposed to live after being observed!). If your system needs more RAM, it needs more RAM. Why would you care about which part needs more RAM? If you have an answer to this question, and your answer is targeted towards one specific service, then you don’t necessarily need microservices for that. A single split in the monolith would be enough. If you do have an answer that relates to all services, that’s an actual reason to use microservices, though it may not be a common case.
Horizontal Scaling
This is where microservices shine by default. However, a modular monolith can be horizontally scaled as well, by spinning up multiple instances of the monolith and load balancing the requests between them. This is not as granular as scaling individual services, but it is often enough for most use cases, and still much simpler than managing a network of microservices.
Latency
Microservices introduce a lot of overhead in terms of communication latency. We’re comparing something like a function call to a combination of one or more network calls, plus payload serialization and deserialization, plus potential TLS overhead, times two. Even if the network calls are fully emulated and there’s no TLS, there should be at least an order of magnitude difference in terms of communication latency, let alone the actual network overhead. This should be very carefully considered in your evaluations. For example, this is an absolute deal-breaker for real-time systems that have even the least amount of need for communication.
Communication
When microservices communicate with one another, a simple function call in a monolithic system becomes a network call, requiring a network protocol, serialization, message brokers, or even a service mesh. This makes your system as a whole harder to debug. Not only do you need to debug the functionality of different parts, but you also need to debug the communication between them, which most likely would otherwise be a push into a call stack. It’s worth reminding ourselves of the “crooked wall” proverb here!
Data Consistency
If you implement microservices correctly, you will most likely need to duplicate some data (microservices by definition are loosely coupled). The data consistency that was once the responsibility of the RDBMS is now the responsibility of the developer. This is a huge, unnecessary burden. Apart from this, if you ever need to join two tables, you’re in for a treat! You need to reinvent some of the RDBMS features in the application layer.
Languages
If you need to use different languages, that could be an actual benefit of microservices over modules, depending on your tech stack, since some technologies might allow the easy use of different languages in the same project. However, one should ensure that this is an actual need. Diversity might sound cool, but it is not what you want in software. It is even painful at the level of different timestamp types, let alone different languages for each subsystem.
When Should You Consider Using Microservices?
Here are some cases when you might consider using microservices. It is important to note that these are not the only cases, and the final decision is context-specific and comes down to your own evaluation.
You may consider using (micro)services when:
The System Already Consists of Microservices
If it ain’t broke, don’t fix it, unless you are planning a rewrite.
There Are Already Separate Teams, Each Proficient in a Different Tech Stack
To avoid the overhead of teaching another team the new stack, it may be better to consider the trade-off of pushing that overhead into the communication between the services, which itself doesn’t “require” going all the way to microservices. In that case, you can also go for more coarse-grained systems.
⚠️ Heads up!
If there are no teams yet, but you’re planning to have them, it would be wise to plan to use a single language if there is no severe need for multiple languages. As mentioned above, technology diversity for the sake of diversity brings nothing but problems. Yes, the teams won’t need to know what happens in other systems, but that’s only true until a change in the team happens. Teams are not immutable! If there are multiple teams with different stacks in the future, unless you have a clear, strong reason for it, stop it and decrease some pain for your future.
You Want To Host Already-Made Services
If you’re planning to use a service that is already made, you won’t have a better option; use that part as a separate service. This won’t turn the entire system into a microservices architecture either, as long as other parts of the system are not separated in the same way.
The Tooling Isn’t There
When the tooling you need for a particular task isn’t available or isn’t good enough based on your requirements in the language you’re using, and you really need to write one part in a particular language, go for it, with caution.
You Want To Increase Job Security by Introducing Complexity!
No explanation is needed! Please don’t!
Conclusion
Microservices have gained significant popularity, driven more by hype than necessity, and in most cases, they introduce more complexity and overhead than they resolve. A modular monolith offers the same benefits of independence and separation without many of the additional side effects that microservices tend to introduce.
In tech, everything is an “it depends” problem. Always evaluate first!
A good rule of thumb is: Unless your specific use case demands the unique advantages of microservices, it is wiser to stick with a well-structured monolith or any closer alternative (e.g., coarse-grained systems) that can satisfy the needs with fewer side effects, which would probably cover most cases. Remember, a solid foundation is crucial; laying the first brick correctly is the first step to avoid building a crooked wall.
The Feedback
The internet is a wild place! In less than 12 hours after the initial publishing of this article, it received around 17 thousand views and 260 comments. The feedback was quite unexpected, ranging from complete agreements to strong disagreements, ad hominem attacks, and even plain insults, with very few neutral comments, which is what I liked. Some folks got so angry when they didn’t see the arguments in favor of microservices that they quit reading before making it to the “When to Use Microservices?” section.
Like any other article, this one isn’t flawless. I certainly cannot account for every single combination of tech stack, team size, skill level, communication level, etc. My perception isn’t flawless either. I acknowledge that, and I’m grateful for every single insightful feedback! I dedicated quite some time to this article, revised it, and made some changes that I believe are a better representation of my view on the subject. This isn’t a war, and I’m not here to blindly push my view. At the end of all arguments, there should be an outcome.
I noticed the microservices trend. I concluded it’s not supposed to be this popular due to all the mentioned points, and I decided to do something about it. The frustration from this amount of misuse led me to write this article, hoping that I can share the thoughts forming my point of view. May it change the situation or tilt my point of view for the better. In the end, I do hope the article’s popularity leads to evening out the hype or tilting the “Default” towards modules.