Published: May 24, 2024 | at 10:24 AM • 11 min read

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 wrong. This article aims to clearly explain why you should minimize or eliminate the use of microservices and opt for properly structured modular systems instead.

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 comes 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:

A Google Trends chart from May 2023 to May 2024, indicating the popularity of Microservices, Monolith, TDD, DDD, and Functional Programming. The chart shows that microservices are the most popular term among the others, with the data based on topics rather than literal search terms.

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?

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

Microservices are the Wrong Answer

A better solution by far is to use modules. Modules have been around forever, and they offer a superior approach to addressing 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 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 the auth directory. Implement the AuthGateway interface, and you’re good to go. Meanwhile, Mike’s team can work on the payment directory, and Kate’s team can focus on the sync directory. You have the freedom to do whatever you want within the auth directory, as long as you implement the AuthGateway 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 may seem to have an advantage, but if you properly isolate modules and keep responsibilities and concerns separate, fault isolation can be just as effective, ensuring correctness by testing the contracts.

Runtime

I suppose 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. This is not a benefit, but 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 also the fact that we typically allocate more resources than each service needs. This is necessary for a monolith but 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 clear. The very definition of microservices is to be independently deployable. Even if deploying each service is as easy as deploying a monolith, there are still N of them in a system with a microservices architecture.

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.

Ease of Monitoring

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 be able to say the system is UP . If even one out of your 888 services is down, the system can no longer be called UP !

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 to have 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

Why would you ever want to allocate more resources to one particular part? It’s not like the other parts will eat up the extra resources. If your system needs more RAM, it needs more RAM. Why would you care about which part needs more RAM?

Latency

Microservices introduce a lot of overhead in terms of communication latency. We’re comparing something like a function call to a network call. Even if fully emulated, there should be around an order of magnitude difference in terms of latency, let alone the actual network overhead.

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?

You may consider using microservices when:

The System Already Consists of Microservices.

If it ain’t broke, don’t fix it, unless you plan 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 two services.

⚠️ 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.

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 often introduce more complexity and overhead than they resolve. A modular monolith offers the same benefits of independence and separation without the additional challenges. By focusing on proper modularization, teams can achieve these benefits without the drawbacks. Unless your specific use case demands the unique advantages of microservices, it is wiser to stick with a well-structured monolith. Remember, a solid foundation is crucial; laying the first brick correctly is the first step to avoid building a crooked wall.