Monolith vs. Microservices
Introduction
Often times when the dichotomy between monolith and microservices is discussed, the dichotomy between two somewhat unrelated qualities are overlooked.
Mono means single. And lithic means stone. Which is then often compared to micro, meaning small. And services, well we all know what they are. So we are comparing the quality of something being small (micro) to the quality of something being singular (mono).
I also remember that the main discussion surrounding microservices was about size. Not entirely strange given they used the word micro. Some said a microservice should contain no more than 100 lines of code. Others said it shouldn’t take longer than a day to rebuild it from scratch. So many metrics were discussed and suggested in order to prevent microservices turning into macroservices 😱.
But was this the right focus? Was size really the issue? In this post I want to discuss why I think focussing on size was unhelpful. And what I think it should really be about. To wit we may have a more fruitful discussion about the main problems we encounter when we build or maintain monolithic and distributed systems.
The Crux
The crux is language.
But if thought corrupts language, language can also corrupt thought.
— George Orwell 1984
Language shapes thoughts. And thoughts shape solutions. I’ve often experienced how changing words, syntaxis, analogies or metaphors can make a world of a difference when discussing problem domains.
Mono vs micro
I think the usage of the word “micro” got us off to a wrong start. We started discussing size rather than boundaries. For example, how often have you read or heard someone write or say that an application can be, is, or was too big? I don’t think an application can be too big. I think it is ultimately not about size, but rather about rigidity.
What do I mean by that? The reason why monolithic systems had to, in some cases, be split into separated services was because of differential scaling. While certain modules were fine as they were. Other modules required more granular scaling. The most common form of scaling is horizontal scaling. But when I say scaling I mean scaling in terms of:
- Performance (horizontal scaling),
- Release pace,
- Cost,
- Tech stack, and
- Resiliency.
These, or a combination of these, might all be valid reasons to decide to extract certain parts of a monolith into separate services. But notice, these scaling needs, have nothing to do with size. They are about rigidity. While it’s true that the larger a rigid structure is, the harder it is to handle or manoeuvre. It isn’t actually the size that is really the problem but the inability to be flexible in terms of scaling in the above mentioned area’s.
Others have argued that the size refers to the scope of responsibilities. While I understand the argument. This is really a matter of finding the correct boundaries within a system and being skilled and disciplined enough to set and maintain clear agreed upon boundaries. While splitting them up in different services could help enforce boundaries. I would argue that this would be a too drastic action to achieve this goal. We shouldn’t conflate logical boundaries with physical boundaries. While scaling might require physical boundaries, it doesn’t mean that the boundaries match with your logical boundaries. Let’s say a particular part of the system has different scaling needs apart from the rest of the system. It would be wise to consider whether a subset of a single logical boundary, exactly a single logical boundary or whether it is expedient to physically separate multiple logical boundaries. This entirely depends on the context in which there are different scaling requirements.
Mono vs distributed
Another problem in discussing monolithic vs distributed architectures is that it is discussed as an either/or topic. Which I think is a result of binary thinking. While it is true that the moment you extract a certain part, module or modules out of a monolithic system, you can strictly no longer speak of a monolith. When we decide to extract a module or part of a module out of the monolith, or build a new service along side our monolithic system, it doesn’t necessarily mean you (have to) abandon your entire monolith and move everything to “micro services”. Instead we would just recognize that our system has a distributed element/nature.
Mono AND distributed
I think it’s clear by now that it isn’t a matter of one versus the other. But rather, to what extend does your solution benefit from more fine-grained, fine-tuned scaling?
As I said before. I think, when we talk about distributed system, we tend to mainly think about or focus on horizontal scaling. I think the reason for this is because you are asking an engineer. If you would ask someone with more of a strategic focus. They might say, for example, a distributed system would allow for more granular release paces.
Here are some of the ways to achieve similar kinds of scaling (note: not similar scaling results) without having to resort to a distributed system:
Horizontal scaling can be, somewhat, achieved by making your application multi-threaded. Even though, multi-treaded applications are already somewhat the default by now. It doesn’t mean that you couldn’t gain more horizontal scaling by tracing your performance and identify where multi-threading might still give you an extra punch.
Release pace scaling can be done by adopting a vertical slice architecture in combination with feature flags. Where each “feature” has their own code. Where loose coupling is the primary goal rather than a strict DRY approach. This way you know that features are not intertwined and can be released at their own pace.
Cost scaling is mainly possible on a database level. Not needing commercial licenses to solve common data challenges can certainly help reduce overall cost. This might require gaining new experience to be able to effectively and properly use it. Doing a quick cost/benefit analysis should be able to help you decide whether it’s worth the investment.
Tech stack scaling, in a monolith this can only be done, on a database and client interface level. The flexibility will greatly depend on how you have organized your code, data and UI components. For example, you could, for certain features or modules, use entire different database engines depending on the problem they need to solve or licencing costs you want to reduce. Or load UI components using different front-end frameworks using a micro-frontend approach. For example, in cases you want to (be able to) slowly migrate from one framework to another.
Resiliency scaling, in a monolith, can be achieved by employing eventual consistency using a message broker. This allows you to break up your traditional long running or memory intensive transactions, into shorter and low memory transactions. By embracing uncertainty, using BASE semantics and CAP theorem, rather than following ACID principles. When one of the transactions fails, it doesn’t cause everything else to fail. It reduces the chance of race conditions, dead locks and thread pool starvation. This also allows you to fix a bug, which would normally break the entire flow, but now only cause poison messages and allow you to retry the messages and continue the flow as was intended.
Conclusion
The term microservice is not a helpful term when it comes to discussing the actual problems we may encounter when using a monolithic architecture. We would do better to use the term “distributed system”.
It’s not a matter of monolith versus a distributed system. You can combine the two and employ patterns and technologies which are generally more associated with distributed systems in your monolithic application.
We move the needle forward when it comes to utilizing distributed processing by improving:
- Terminology and information
- Programming languages and framework capabilities
- Tooling
- the marriage between monolithic and distributed architecture
- Platforms hosting distributed systems
- Documentation of patterns, trade-offs and rules-of-thumb
Remember, scalability is a quality worth pursuing for your architecture. But it’s not a goal on itself. Even though scaling challenges makes any engineer’ heart beat faster. Delivering business value at the right pace is the real goal. And the correct scalability can help you achieve the said goal.