As the system (such as a mobile application) grows and evolves (hopefully with the team!), it is becoming challenging to expand, manage and update the code - fulfilling incoming business needs and solving user-critical issues will typically get harder for the team and take more time than it used to, when the product was smaller. Multiple factors contribute to this condition, but in this article, I want to focus on the monolithic codebase, which is one of the factors, and explain how applications can benefit from modularisation.
Monolithic vs Modular Architecture
What does the monolithic codebase refer to in the first place? In simple terms, it is a type of software architecture in which the entire application is built as a single, unified system - with all of its parts developed and deployed simultaneously.
Modularisation refers to the process of breaking down a software system into smaller, independent modules that can be developed, tested, and maintained separately. The concept of modularisation has been around for decades, and it is widely used in software development, including mobile applications.
In this article, I will go over why splitting a monolithic codebase can be beneficial while building products at scale and how it is worth considering even when building MVP.
Why can a monolithic codebase be a problem in the first place?
One word - coupling.
Of course, there is much more to it than just this simple answer. Still, a high degree of coupling in the monolithic codebase is inevitable because all pieces of code can share the same abstractions, use the same domain models, and depend on the same set of services. All at the fingertips of an engineer modifying some part of code, with no hard-to-cross boundaries that would discourage one from connecting two pieces that should not be connected or making the required connection too tight.
Tight coupling can have serious consequences that span the business:
- introducing changes (say adjusting business process mapped to some feature) becomes more time-consuming (each connection must be checked to ensure that this other part is still working as expected)
- introducing bugs becomes easier (a single change affects multiple places)
- contributing to the codebase as it grows becomes harder (the work of one engineer affects others - there are more conflicts, more uncontrolled, breaking changes in the APIs) - so you choose between growing the team (to keep the pace up) or slowing down (exponentially! - think about the number of diagonals increasing along the number of edges in polygon)
How can you benefit from modularisation?
On codebase level
Having more focused pieces of service makes the entire system much easier to maintain - with well-defined modules, introducing changes (say implementing new business requirements or fixing bugs) becomes faster, less error-prone and can be done without extensive knowledge about the entire system - understanding how a given subset of modules works will usually be more than enough to introduce the change.
Daily development also gets easier, as we can run unit tests only for the module we are interacting with - and this is great because even in a vast codebase, TDD is still possible. At the same time, in the monolith, the purpose of it would be buried by long compile times. Additionally, we can introduce internal 'apps' that launch only some features that we are interested in - which can be used both for development and manual tests to avoid the burden of going through onboarding and verification flows when we do not need to check them.
On infrastructure level
Once we split our application into much smaller modules, which can be tested independently, we can also speed up our Continuous Integration workflows . Rather than running a whole set of unit and integration tests every single time one of the developers opens a pull request, we can instead detect which modules changed and only run test suites for them. This can introduce great cost savings (think about a team of 40 engineers producing 2-4 pull requests daily, each saving us dozens of minutes on CI servers) and encourage smaller pull requests even more as a bonus.
What's more, modularisation also makes it easier to introduce alternative build systems which feature caching of the modules that have not changed from the previous run, such as Buck or Bazel - which improves the developer experience and reduces infrastructure costs even further.
On team level
This one is my favourite - with those hard-to-cross boundaries in place, we make it much more challenging to introduce unintended coupling. We encourage our team to make conscious choices whenever they need to reuse code, connect two separate modules or expand existing connections. Moreover, we can graphically express dependencies between those modules, which can greatly contribute towards choosing the right approach for a given problem spanning across modules (i.e. should we use Dependency Inversion or Dependency Injection to implement the binding between the two).
Another benefit is that it is much easier to introduce new technologies or propose technical innovations in the codebase - simply because we can scope such change to a single module, which makes such decisions easily revertible, thus, much cheaper. This allows a much higher degree of autonomy within the teams, contributes toward the growth of its members, and increases the sense of ownership.
Speaking of ownership - designing 'optimal' team topology gets much easier with modular service because rather than having the platform team split artificially based on 'each scrum team must have 1 iOS engineer' kind of drivers, you may consider the importance and size of particular business processes (translated to a given subset of modules), and adjust team size as well as its competences accordingly. Compare it to the monolith, where your entire engineering team is stepping on each other's toes - super counterproductive!
On business level
All of the points above affect the business as well, but there are two more potential benefits that I wanted to mention - scalability and reusability.
First of all, scaling a modular system is much easier than the monolith - a smaller group of engineers can expand and add functionalities with less effort, as we have clear boundaries between well-architected processes. Moreover, it becomes more obvious which modules will get affected by the given adjustment.
As for reusability, if our company is working on multiple products, it is not uncommon for those products to make the user go through a similar set of processes (i.e. onboarding, if we want our users to switch between products seamlessly) or use the same design system supporting our brand - modular codebase makes it much easier to share existing code between the products, which can lead to huge cost savings, especially if we are planning this kind of reuse from the beginning.
Is modularisation always a good choice?
I might have convinced you that from certain angles, modularising system at scale comes with many benefits, but is that always the case? What about starting new products with a smaller team? Is it still worth thinking about modularisation from the beginning? There are numerous excellent articles on this subject, that support this claim, such as post by Stefan Tilkov published on Martin Fowler’s blog. But in the end - it depends, as usual.
The first thing to consider is what kind of product you are developing. For example, if you are working on a Proof of Concept that will only serve the purpose of verifying some thesis (business or technical - it does not matter), my suggestion would be not to modularise at all, as you want to go as fast as possible, cutting corners wherever you can to reduce the cost - the service you will create is likely to be thrown away anyway, as it is not meant to be transformed into a fully-featured product.
Another driver for such a decision is your team composition - do you have someone who has done modularisation in the past and knows how to avoid common pitfalls and implement it in a way that will allow you to get the benefits? Do you have someone who knows how to make necessary adjustments to the tooling you would use in the monolith to make the dev experience seamless? If you don't, you should also stick to the monolith.
One more aspect that you should consider is how well you know the domain and the business - the deeper your understanding of them, the better you can design the modules and boundaries between them. If you don't have such knowledge, my suggestion would be to modularise at the edges of the system - from domains that follow some well-defined archetype, such as caching, networking or persistence - and delay modularising the core domain of your business until you get a better understanding of it - otherwise, you might not be able to model your modules correctly.
In the end, the system architecture is just one of the factors contributing to the business's prosperity (or failure!). You can be hugely successful with a monolith as well as with modular service - the most important thing is to consider crucial drivers at the given time and make conscious decisions based on them. A great example of such an approach is the Prime Video team, who has decided to move its distributed service to the monolith to reduce the costs of running it.