The microservices fallacy - Part 9
In this post we take a look at moduliths as an alternative to microservices.
In the previous post we started to make sense of what we have learned from this blog series so far. We discussed when microservices are a sensible choice and what other conditions must be met to harvest their benefits and not only experience their downsides.
We then started to discuss what to do if the required preconditions for microservices are not met, and I wrote that I will discuss two alternative architectural styles in more details. The first alternative are moduliths which are the topic of this post.
- do not need to move fast and/or do not have multiple teams (and exponential scaling is not in sight)
- and do not have any special runtime requirements in terms of very disparate NFRs,
a deployment monolith is the probably simplest runtime architecture possible.
They key problem of a deployment monolith is that its internal structure tends to deteriorate over time which makes evolution and maintenance a nightmare. Yet, this is not a “given” of a deployment monolith, even if we experience it so often. This is rather a question of good design and implementation discipline.
Hence, it would be an option to go for a well-structured monolith, a “modulith”, a monolith consisting of clearly defined and isolated modules. Still, “well-structured” does not come for free.
First, it requires decent design skills to find the right module boundaries and contents, to decide which functional concept to implement where and how to expose it to the outside.
Second, it requires that everyone adheres to the rules, not to violate module encapsulation, no matter if accidentally or deliberately. This especially means to access modules only through their official interfaces – also if you are in a hurry to deliver a feature and you could save time by cutting corners, i.e., by bypassing module interfaces.
These are the hard parts – getting the design right and then adhering to it over time. This is also the reason why we see so few well-structured monoliths. Good design and implementation rigor are so often sacrificed for some arbitrary minor short term goal.
For implementation rigor sometimes some “hacks” are useful that make it harder to violate the agreed upon design. E.g., in a Java world you could put every module in a separate JAR file that is developed in a different project and assemble the modulith from them at build time.
People might complain that this would make testing harder, would make code tracing harder, and so on – all the things that are so easy from within your IDE if you work with a weakly structured monolith.
Well, but remember that the proposed alternative are microservices. With microservices all this is impossible by default and nobody complains. Quite the contrary, most people yearn for them even if they do not need them.
Additionally, if it makes testing noticeably harder it is a sign of weak design. Good design results in weakly coupled modules, also from a functional stance. Thus, you should be able to test the modules in isolation and apply test techniques like contract-driven testing. If isolated testing is not possible, it is a sign that you should revisit your design.
Furthermore, the need to trace code across module boundaries usually is a sign of poor interface documentation. If I use some other functionality, no matter if provided as library, service, JAR or however, I do not want to read the code of it. I want to read the contract and then know how to use it and what to expect from it. Period. If usage or behavior are unclear, either the documentation or the interface design itself is insufficient. 1
Technology updates with moduliths
We still have the issue of technology updates. Even if they are not needed often, they can become hard for a really big deployment monolith – but only if the functional design is bad.
The internal functional design controls how hard it is to replace a piece of software with a new technology. The more tightly interwoven the internal parts are, the harder it becomes, the weaker they are coupled, the easier it becomes.
If your internal design is driven by reusability, you typically end up with lots of modules (re-)using each other. If you do it well, you have an internal layered module structure where each module of a higher layer builds on the functionalities provided by the layer below. 2
If your new technology can call the old technology in such a setting, you can replace the application top down. If the old technology can call the new technology, you can replace it bottom up. If both directions work, you are free to decide.
Yet, if you do not create a good layering but an unstructured mess of tightly coupled modules, any technology update will become hard.
Still, be glad then to have a deployment monolith and not microservices. In the context of microservices unstructured tight coupling would mean a distributed ball of mud which is the worst of all worlds: Hard to maintain, hard to change, hard to deploy, brittle at runtime and hard to operate. With a weakly structured deployment monolith at least deployment, robustness and operations at runtime would be better.
The key point is technology updates do not become hard due to the monolithic runtime structure, but due to deficiencies of the internal functional design and implementation.
Another option is to drive the internal design by mutual independence. This leads to a use case driven modularization where the different modules encapsulate whole use cases or user interactions: They handle external requests end-to-end without having to call other modules. Visually speaking, instead of layers you have pillars. In such a design, you can replace the system module by module because the modules are mutually independent. 3
Still, even with a design driven by mutual independence, you will still have functionalities where it economically makes sense to create them as reusable parts instead of recreating them for every module. For these functionalities, we are back to design driven by reusability which we already discussed.
Summarizing, it can be said that technology updates will probably be a bit more complex with moduliths than with microservices. But a good functional design can keep the additional efforts small. Often it is worth to pay this little extra price because on the other side you avoid all the additional complexities that come with microservices.
If you do not need to go fast and/or do not have multiple teams and do not have very disparate NFRs for different parts of your application, a modulith can be a sensible alternative to microservices. It offers most advantages of microservices like better maintainability and evolvability, team independence and more while not exhibiting the intricacies of microservices at runtime. A modulith is still a deployment monolith, i.e., one of the simplest runtimes architectures available.
A modulith requires advanced functional design skills and strict implementation rigor – two properties that unfortunately are not seen very often in most companies. If you do not have the two skills in your company, most likely you will up with the dreaded big-ball-of-mud-type monolith.
Yet, this is still better than a distributed big ball of mud that you would end up with if you were going for microservices while lacking the two aforementioned skills.
In the next post of this series (link will follow), we will take a closer look at the second alternative I want to discuss: Microliths. Additionally, we will look at some problems that you might face if you decide for one of the discussed styles and what you can do about it. Stay tuned …
I will discuss interface design and documentation in more detail in future posts. Right here I only want to stress the fact that you waste a lot of time and money if you fail to document your interfaces properly. Every user of your module has to read and understand your code arduously trying to figure out the module contract because you failed to document it. Thus, the time and effort you saved will be paid a hundred times or more later by the poor people who have to use your module. ↩︎
This is not to be confused with the dreadful, widespread “layered architecture” anti-pattern where data access, business logic and presentation are considered “layers”. This rather resembles a layering concept like the ISO/OSI networking framework where each layer builds on the API of the layer below, adds functionality and provides a higher level API to the next layer above. I will discuss in a future post why the widespread implementation of “layered architecture” with data access, business logic and presentation layer (or alike) is an anti-pattern. ↩︎