We all know what monoliths are, right? Technically, a monolith is an application made of just one executable unit. An all in one solution. That's one ideal definition. Now, more often than not, a monolith is a software made of tangled 🍝, overcomplicated 💢 and unintelligible 😖 code that almost everyone had the pleasure 🙆 to work with. You may find this architecture in many many companies (mostly companies with more than ~10 years of life) which started their applications as monoliths (because of the architecture advantages during early phases of the application development) and never had the possibility nor the necessity to change it. I bet every developer reading this have worked with something similar 🤓.
Indeed, we usually tend to think a monolith as a big and bored legacy system. Partly because nowadays we love talking about trending architecture patterns like microservices or event-driven, leaving our obsolete monolith behind. Besides, a monolithic application, without proper architectural restrictions, is easily injured by our work as developers in the industry ⇒ In order to deliver fast, we may create technical debt (we add an "exceptional" one-time dependency, we break the strict layered division, and so on...), and that debt is rarely paid. Furthermore, once you add some technical debt, it seems less dangerous to add some more in the next task (Stop! Be brave! Or be a Lannister and pay your debts!). And then, the exception becomes the rule in your codebase. At the end, we may have a big ball of mud in front of us.
Of course, this may happen to any other system with a different architecture style, but being a monolith makes it really easy to break some flexible laws. It's just a method call away.
However, that's not the whole story...
Monoliths doesn't have to be that complicated! There is a nice, old, and well-known technique that could organize the monolith properly, and that is simple modularization. Of course you heard about it. We apply it in a day-to-day basis when creating different classes. We are always usually splitting responsibilities among objects (divide and conquer, right? 😃). Now, the idea is to do the same but in a bigger scope. In an application (and architectural) scope. I will try to briefly explain it along with some useful concepts.
With a modular architecture, you will have all different functionalities encapsulated in modules (hidden information) within other super related (cohesive) functionalities communicating with other modules (coupling), ideally, through well-defined interfaces (loosely coupled). We should be aware of this communication between modules, as it generates a dependency. Dependencies are a good way to infer the impact of the changes in your code and we may brainstorm a lot about them (but let's do that in another post, please 🙂 ). This architecture style is known as modular monolith, and good practices around it reduce overall complexity.
With a modular structure it's easier to manage restrictions between inter-module communications, for example, developing some fitness functions. You will have order. If the team accomplishes to keep a low coupling, a reduced chat through proper entry points between modules and a proper inner-module structure in each one of them, the whole picture will look less scary. The codebase will be more understandable, thus, happier and more efficient development.
Benefits of modularization are not only technical, but organizational. It's a good way to scale the teams. With several modules, we may have different teams in charge of only a specific (and again, related, cohesive) set of them. In case a module keeps getting bigger and bigger, you may split it and create a new team to tackle it, either by refactoring it or decomposing it in smaller modules. Furthermore, proper modularization improves inter-team communication, as they should only be in touch with depending and dependent modules's owners. Again, teams will focus only on their functionalities without worrying that much about the whole picture (let's upper technical leadership take care of that, right? 😜). This functional division may remind you of one of the benefits of microservices, which gives more autonomy to each team.
Of course, only having modules is not enough, but it's a first step towards a healthier codebase. The next step is to leverage each module while keeping dependencies, coupling and inner structure in place. Indeed, it will not be that helpful if two modules have a lot of dependencies (and worse, cyclic 😱) pointing to several of each other's internal objects. That will break encapsulation and every tiny change you may want to do to any part of the module might have a huge impact and unexpected side effects.
I know, I know, this is a pretty high level overview of a Modular Monolith and its core concepts, which are easy to write. Decomposing a monolith into proper modules is not easy task though, neither to offer proper interfaces and keep dependencies flowing only through them.
- How to separate functionality?
- How big/small a module has to be?
- Can we measure module granularity?
- Is this module cohesive enough?
- How much coupling?
- What if I have a lot of dependencies?
- My modules are really chatty, what should I do? ...
Well, these are the real questions, and I'll try to write my thoughts about some of them in next posts.