Modularization is a technique that allows you to break your monolithic Android application into several independent… well, modules. You can think of a module as a standalone sub-project inside the codebase. While it might not be obvious why you’d like to do that to your project, in some cases modularization can be extremely useful.
In this article I will describe when and why you might want to modularize your Android application.
Misuse of Modularization
I feel obliged to issue a warning before we get to the main topic of this post. See, in my experience, modularization seems to be misused much more often than it’s used correctly. In essence, most of the modularized codebases that I saw suffered from one or more of the following problems:
- Preliminary modularization
- Modularization along non-optimal boundaries
In my previous post, I explained the above issues and discussed several examples which show their negative effects.
Based on my experience, I believe that many developers who will read this article either don’t need to modularize their apps at all, or already have non-optimal modularization strategy in place. Therefore, in some sense, that previous post that describes the pitfalls of incorrect modularization is more important and useful than this article. I recommend reading it before proceeding any further.
Once we understand and agree that modularization can be misused and cause harm, let me get back to its positive aspects.
Separation of Application’s Sub-Domains
Each application targets one or more specific business domains. These domains are often called problem domains because most apps are written to solve specific problems.
For example, e-commerce apps allow users to shop for stuff without visiting a physical store. So, that’s their business domain: online shopping. However, to allow the client to pay for products and then receive the purchases, there is quite a bit of additional machinery that comes into play.
For one, online monetary transactions are tricky. Sure, e-commerce apps don’t necessarily need to be fully-fledged payment gateways, but, still, they’ll usually have a considerable amount of logic to support online payments. In addition, more functionality is usually required for features like invoices, cancellation, history, etc. In short, even though e-commerce apps look like catalogs of products, under the hood they implement quite a bit of logic around online payments business domain.
So, e-commerce apps need payments. Similarly, chat apps might need encryption, running apps might need location and maps, etc. You get the idea.
These “secondary” domains that you often need in your apps are called sub-domains. Well, not just “secondary” domains, to be precise. As your application grows in size, some of the core parts of your main business domain can grow into their own sub-domains. For example, “product” in big e-commerce applications can grow into big and involved sub-domain on its own.
When you have a clearly defined, mature sub-domain in your application, you can take advantage of this fact by extracting the logic of this sub-domain into a standalone module. This will make inter-domain relationship more explicit and reduce the complexity of the codebase. Now, you’ll know that you don’t need to deal with that part of the application if the change you need to make isn’t related to that sub-domain.
However, please note that I wrote “clearly defined, mature sub-domain” in the previous paragraph. These criteria are very important. If you extract logic into standalone module and it turns out that the abstractions you defined are incorrect or unstable, that’ll most probably be very wasteful and can have very negative long-term effects. Therefore, wait with modularization of sub-domains until you clearly see stable abstractions in the code.
Code Ownership Between Teams
On bigger projects, there might be more than one team contributing to the codebase. In such situations, it makes sense to introduce clear boundaries between teams.
So-called Conway’s law captures this idea at a higher level of abstraction:
Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.
To be honest, I don’t know whether Conway’s law describes a desirable outcome, or a limitation. However, in either case, if that’s the eventual outcome, it makes sense to formalize this structure as quickly as possible to avoid confusion and congestion.
The best way to divide the ownership over codebase between different teams is to break the system along the boundaries of sub-domains, as described in the previous section. However, that’s not always possible in practice due to multitude of reasons. Therefore, you’ll need to take into account many variables to implement modularization strategy that will allow different teams to work largely independently, while also not obscuring the core domain architecture of the application.
Incorporation of Third-Party Code
In some situations, you might want to add big chunks of third-party code to your project. One relatively common example is when some external open-source library that you want to use doesn’t have that one small feature. In this situation, you might decide to copy the code of that library into your project and change it to accommodate your requirements. It’s not something I’d recommend doing often, but, in some cases, it’s the optimal solution.
Whenever you find yourself doing that, your best bet is to add the external code to your project as a standalone module. This way you’ll make sure that you don’t create excessive coupling to the external code. In the future, if the requirements will change, or that feature that you implemented will be added the original library, you’ll be able to remove this code from your project relatively easily. In addition, you probably don’t want to deal with that code when maintaining your app, so having it in a standalone module will allow you to pretty much ignore it after you add the required functionality.
If you need the same functionality in multiple applications, modularization is your go-to solution. Whether you’re working in monorepo and all the apps can just import that module right away, or you want to use git sub-modules, or you’re preparing to extract this functionality into standalone project and release it as a library to your local maven repository, all these use cases scream: “standalone module”.
If you’ve got a messy codebase and would like to clean it up, most probably it’ll take quite a bit of time (months or even years in some cases). In this situation, it will be very useful to extract all the clean code (either refactored or newly written) into standalone module(s). This way you can gradually refactor the code, change coding conventions and track progress without making big and risky changes.
Experimentation with New Technologies
Another valid use case for modularization is when you want to add new technology to your project. It might be just new to you and your teammates, or completely new by itself. In either case, there are risks to the project. To mitigate these risks, you can extract a standalone module and use the new technology only there for couple of weeks or even months. This will allow you to dip a toe into that tech with minimal risks.
In this context, I can’t avoid mentioning Kotlin.
Google put their entire PR budget to promote Kotlin, but never told Android developers about its drawbacks. One of these drawbacks is much longer build times, especially if Kapt is used. Many projects switched to Kotlin right away and added Kotlin code alongside existing Java code in a very unstructured manner. After all, Kotlin has good interop with Java and Google recommended that we all use it. Well, following Kotlin’s adoption, many projects had serious issues with build times. Therefore, they had to either live with that and sacrifice some productiviy, or invest additional effort to mitigate this new problem. In either way, it resulted in productivity hit.
But there were also teams who decided to test Kotlin in dedicated modules and keep the existing functionality in pure Java. These projects had much easier time adopting Kotlin and they didn’t need to struggle as much with build times.
Instant Apps and Dynamic Features
I don’t have experience with either instant apps or dynamic features, so I can’t discuss these use cases in details. I just know that both require modularization to support delivery of partial functionality to the users.
That said, in my opinion, you should avoid adding modules “because in the future we might want to make this into an instant app”. That would be the classical case of over-engineering and preliminary optimization.
I already discussed build times very extensively in the previous post (when I warned about the downsides of unneeded modularization), so I won’t go into much more details here. Just keep in mind that if you modularize due to one of the (valid) reasons described above and your modules aren’t too small, it should also help you with build times. Therefore, theoretically, you shouldn’t modularize just for the sake of improving build performance.
However, in practice, many projects face issues with build times specifically and extract modules just to address this problem. That’s probably alright, as long as you don’t go nuts about it. Big companies that employ tens of developers like Uber, Lyft and Grab often end up with hundreds of modules in their Android applications. Then they start eyeing Buck/Bazel becaues Gradle “doesn’t scale”. That feels like over-modularization to me, but I’ve never worked on projects of that size, so I can’t test this hypothesis.
That said, since most Android projects are way smaller than 500 KLOC, the practices employed by Uber, Lyft and Grab aren’t very relevant to most of us. Therefore, in my estimation, there should be no need to extract the logic of each single screen into standalone module, or employ any other approach that results in over-modularization. 10 KLOC sounds like a reasonable number for the minimum average module size in Android applications. If you get below that, it’s time to pause and take a critical look at what you do.
In this post I described how you can take advantage of modularization in your Android projects. That’s very useful technique and its proper utilization can have a major impact on the maintainability of your codebase.
However, keep in mind the disclaimer at the beginning of this article. Modularization has its costs and its not a silver bullet. Therefore, you shouldn’t modularize your application before you see a clear benefit of using this approach in your specific case. Otherwise, modularization can result in negative gains, which is, unfortunately, was quite common in projects that I saw.
As usual, thanks for reading and please consider subscribing to new posts notifications if you liked this article.