Often, inexperienced teams want to create micro-services but end up with a distributed monolith. What is it and why should you avoid it
First, we need to understand the difference between a simple monolith and micro-services.
Impressive only by name, monolithic is in fact the most common architecture style. It's when everything is contained in a single piece, or in other words, self-contained.
In practice, a monolithic backend looks like a single and large codebase that provides all the APIs you need.
Now, the questions are, why is monolithic the traditional architecture, and why would you want to create micro-services instead?
A monolithic architecture comes with much more simplicity at the beginning of a project and is sufficient for most projects.
When your whole codebase is in the same place, development, testing, monitoring, and deployment are much more straightforward, faster, and cheaper.
But when your application grows, with more functionality and users, new challenges appear. They could be gathered under two main categories, codebase maintainability and scalability.
In a monolith, it's a common practice to organize code by creating abstractions, such as modules.
But those abstractions and their boundaries usually break down with time, and similar code starts to become spread all over.
With a large codebase, it becomes difficult to know where a change needs to be made, making it harder to fix bugs and implement new features.
Then, come the issues with scalability. When a system needs more resources, there are two ways to scale it: horizontally, and vertically.
Vertical scaling refers to adding more resources to an existing machine, such as GPU or RAM. Horizontal scaling refers to spreading out your application on multiple machines and adding more machines to your pool of resources.
Vertical scaling can be enough but will be limited at some point, as there is a limit to how much resource a single machine can have.
Horizontal scaling is in theory infinite. On the other hand, it requires your application to be able to work when spread out on multiple machines.
Even a monolithic application could work if it's spread out on multiple machines, as long as it's stateless. But monoliths have a weakness that micro-services don't have when it comes to horizontal scaling.
As a monolith contains the whole system in a single codebase, everything must be scaled together. If you only need to scale a subset of endpoints, you have no choice but to deploy the whole system.
Unfortunately, microservices are described in a lot of different ways.
A succinct definition I particularly like comes from "Building Microservices" by Sam Newman:
Microservices are small, autonomous services that work together.
That's enough for a first approach but lacks details to understand microservices at a deeper level. From "Microservice Architecture", we learn microservices should share the following traits:
- Small in size
- Messaging enabled
- Bounded by contexts
- Autonomously developed
- Independently deployable
- Built and released with automated processes
Now, what does that mean, in practice? In the real world, your app is split into multiple services with its own set of related functionalities (while ensuring low coupling between them).
There are, at the same time, multiple instances of the same service depending on the load to handle. They communicate asynchronously to ensure messages get delivered and avoid coupling.
Opposite to monoliths, why is development, testing, monitoring, and deployment slower and more expensive with microservices?
New challenges appear with multiple applications. For example, managing data in separate but related databases comes with more challenges than a single, unified database.
Then, you need some sort of communication between services, usually through an event bus. Making your micro-services work together, testing them correctly, as well as deployment will be a serious headache.
In the end, you should expect development to take more time with micro-services. The increased complexity also means you need a bigger team to manage it, setting up micro-services with a single and small team is a bad idea.
There is one primary benefit with micro-services. They can be easily deployed as you need them, and scale more efficiently.
With micro-services, you can just scale the service that needs scaling, making it possible to run it on smaller, less powerful hardware. It makes it faster and more cost-effective.
Micro-services provide other benefits compared to monoliths, such as resilience, ease of deployment, and replaceability. As a monolith contains your whole app, if one of its components fails, everything else becomes unavailable.
Also, you might have experienced how hard it can be to refactor a huge codebase, whereas the size of a micro-service is usually limited. The cost to replace a service is then much more manageable.
On large projects, with numerous teams, micro-services might even have a positive impact on productivity
A distributed monolith is the result of splitting a monolith into multiple services, that are heavily dependent on each other, without adopting the patterns needed for distributed systems.
In practice, it's the result of splitting a monolith into separate services, but keeping them tightly coupled.
That means, they still rely heavily on each other. In this context, you lose the simplicity that comes with a monolithic architecture, but don't enjoy the benefits of independent microservices.
Don't do it!
Being lowly coupled doesn't mean you have absolutely no relationship either. For example, you might send and listen to events.
Also, you should write some integration tests, or to be more precise contract tests (if you're doing microservices well).
That means, at some point, you need information on the contracts from other services. There is some relationship going on, but it doesn't make your services tightly coupled.
Do you want to learn more about micro-services?
Good news, I teach micro-services in a practical course.