Android Modularisation Strategies
Discover effective Android modularisation strategies to boost scalability, maintainability, and build speed. Learn the pros and cons of layered, feature-based, and hybrid modularisation to optimise your Android development workflow.

What is modularisation?
Modularisation is all about breaking your Android app into smaller, self-contained modules. Each module tackles a specific job or feature, keeping things neat and organised. By defining clear boundaries between modules, your app becomes easier to maintain, faster to build, and simpler for your team to work on together.
What are the advantages of modularisation?
There are quite a few potential advantages to modularisation depending on your use case and context.
- Faster builds: Modules compile independently, leading to quicker incremental builds.
- Enforces separation of concerns: Clearly defines responsibilities, reducing complexity and avoiding tightly coupled code.
- Improved maintainability: Smaller modules with clear boundaries make code easier to understand, debug, and maintain.
- Parallel development: Teams can work simultaneously on different modules without stepping on each other’s toes, reducing potential conflicts.
- Enhanced testability: Isolated modules simplify unit and integration testing.
- Better scalability: Easier to add new features and scaling or reusing existing ones independently.
- Easier onboarding: New developers can quickly understand individual modules rather than tackling the entire codebase. Team members can even split responsibilities to certain modules independently of others.
Are there any disadvantages to modularisation?
Modularisation is not an immediate best practice in all cases. Developers should know multiple modularisation strategies and pick the best one for the project and context they find themselves in.
Here are a few rabbit holes you might fall into with improper use of modularisation:
- Increased complexity: Managing dependencies between modules can become complex as the number of modules grows and can trigger cryptic and hard-to-debug errors.
- Initial overhead: Setting up and maintaining multiple modules adds initial development and configuration overhead.
- Potential duplication: Without careful management, some logic or utilities may become duplicated across modules.
- Dependency management issues: Risk of creating cyclic dependencies or overly complicated dependency graphs.
- Navigation complexity: Navigation between modules requires careful planning to avoid convoluted routing.
Modularisation approaches
At the beginning, your choice of modularisation strategy might not seem like a big deal, but its impact will become crystal clear as your project expands and your team grows. This decision can absolutely make or break your app down the line.
I usually pay close attention to the project's context and its medium-to-long-term goals because this decision can either sustain steady, safe progress or severely hamper development speed and developer satisfaction.
Now let's explore a few common strategies together.
No modularisation approach
As the name suggests, the simplest modularisation strategy is no modularisation strategy. 😅
In terms of Android development, this means relying on the good old app
module that comes with every new project and not creating any other modules.

Unfortunately, this approach lacks most of the advantages we've discussed earlier. Build times will not be faster as parallelisation cannot happen, separation of concerns is not enforced as all the code sits in the same module, it might be hard to spot boundaries between features and having multiple developers working on the same module can more often lead to conflicts.
There is one big advantage to this approach though: simplicity! You will stay well away from dependency hell, you will have less overhead and faster development time initially, no risk of cyclic or inter-module dependencies and everything will be easier to refactor if needed.
There's a reason I included this strategy: its simplicity and minimal overhead might be perfectly suited for small, short-term projects (like proof-of-concept apps) or solo developers who prioritise speed over enforcing strict rules or managing conflicts between contributors.
Don't dismiss or underestimate this approach because of its simplicity. Simplicity is its greatest strength!
Layer-based modularisation
If you are developing any kind of serious Android app and you have some experience as a developer, you are most likely following some sort of architecture to ensure programming best practices and standards are met.
Usually these architectures tend to split the codebase into layers, each responsible for their own type of work. Does it sound familiar? Where did we hear that before? 😅
As you can imagine, using dedicated modules for each layer is a common approach and a decent utilisation of modularisation.
To take a look at a practical example, let's imagine an Android codebase split into three layers: presentation, domain and data. Presentation and data layers can know about the domain layer, but presentation and data cannot know about each other.


We can enforce this rule by creating separate modules for each layer and including only the dependencies they are allowed to use. For example, presentation and data layers would have this line in their build.gradle
files:
dependencies {
implementation(project(":domain"))
// and other dependencies
}
This means you'll never accidentally introduce dependencies that violate architectural principles. For example, you can't directly access a Retrofit interface from the presentation layer.
You also gain the minor advantage of splitting your codebase into three separately buildable modules, enabling parallel builds. However, realistically, it's uncommon to develop a feature confined strictly to a single layer. In practice, all modules will likely rebuild frequently during typical feature development.
Feature-based modularisation
In this scenario, instead of dividing the codebase by architectural layers, we split it by features. Each module represents a specific feature, and within each module, we can maintain the presentation-domain-data structure by using separate packages for each layer.

This approach significantly improves build times because only the features you've modified will need rebuilding, saving considerable time spent waiting for the IDE to finish compiling.
It also breaks down the codebase into manageable chunks, reducing potential developer conflicts, especially if responsibilities are divided thoughtfully.
There are a couple of important disadvantages to consider here. First, there's the obvious increase in complexity. Each new module introduces its own source set files, build.gradle
file, Proguard rules, and specific dependencies. At this stage, you'll likely need a core module containing shared resources like UI components, themes, navigation logic, networking utilities, and so forth, which most feature modules will depend upon.
Another subtle disadvantage is that since layers are organised as packages instead of modules, you're free to break architectural conventions if you're not careful. Nothing explicitly prevents it in this setup.
Additionally, if you ever depend on a file from another module's domain layer (for example), you're forced to include the entire module as a dependency, which, in my opinion, is a big no-no. You could move that particular file to a common module, but this quickly leads to random unrelated files being scattered everywhere.
Also, remember that if you move a file from a feature module into the common module, you must also relocate all its dependencies. This rapidly spirals into refactoring hell, wasting significant amounts of development time.
We'll see how we can improve this strategy in the next section.
Feature-based modularisation with layer modules
This strategy is very similar to the feature-based modularisation discussed previously, with one crucial difference: instead of organising layers as packages, they're implemented as separate modules. In this approach, each feature module contains submodules for each architectural layer. But does this solve the issues associated with the basic feature-based approach?
If you define each layer as its own submodule, you can strictly enforce dependency rules, eliminating the risk of breaking architectural guidelines. Additionally, layers within one feature can directly depend on corresponding layers from other features. Revisiting the earlier example, if you require a domain file from another module, you can simply depend on its domain submodule instead of unnecessarily including the entire feature module.
Sounds great so far, but what's the catch? If you thought complexity increased significantly with standard feature-based modularisation, this approach amplifies it even further. You'll face roughly a threefold increase in build files and dependency graphs to manage.
This strategy is quite extreme, and I'd honestly recommend it only for highly advanced projects and experienced teams that clearly benefit from this level of modularisation. Additionally, I would almost never recommend adopting this as the initial strategy for new projects. It significantly slows down early development and introduces substantial overhead for developers.
Final thoughts
Choosing the right modularisation strategy is crucial for ensuring smooth project development. Selecting the wrong one can lead to significant problems, either at the beginning or further down the line as your project grows.
Although transitioning from one strategy to another is possible, it often involves substantial refactoring costs, especially once the codebase has grown considerably. It's best to avoid such scenarios, particularly since you likely won't have the luxury of pausing feature development or bug fixes to focus solely on restructuring modules.
If you're ever unsure about how complex your strategy should be, err on the side of simplicity. Start with a basic approach and only evolve it into something more advanced if and when necessary. A good rule of thumb is: prove you need complexity before paying for it!