The “Real” Modularization in Android

An overview of the principles behind modularization

Denis Brandi
Better Programming

--

Image by author

“If the SOLID principles tell us how to arrange the bricks into walls and rooms, then the component principles tell us how to arrange the rooms into buildings.” ~ Robert C. Martin, Clean Architecture

Should you package by layer or by feature? Is there any other approach?

How can you boost your project’s compilation time?

How can your engineers work independently in cross-functional teams?

With this article, I aim to answer these questions by extending my previous article on SOLID principles.

Table of Contents

Component Cohesion Principles
Component Coupling Principles
Package Design Solutions
Encapsulation
The Main Component

Intro

While SOLID principles can validate and detect code smells in a class or an interface, component principles can validate and detect code smells in a component.

What is a component?

A component is a group of files (classes, interfaces, files of functions, Android resources, etc.) that are grouped using one of the following strategies:

  1. Source code level (monolithic structure): in Java/Kotlin, we use packages
  2. Binary/deployment level: in Java/Kotlin, we use modules that generate “jars” or “aars”
  3. Service level: this would be a service or a microservice, where the communication happens through network packets

Normally, you don’t use just one strategy. You mix these strategies according to your needs.

Since we don’t have services in Android, the article will be focused on Java/Kotlin modules.

What is “good modularization”?

A “good modularization” is a components’ structure in which modules are highly cohesive and lowly coupled.

How can we say that modules are highly cohesive?
And how can we say that modules are lowly coupled?

Modules are highly cohesive when they adhere to component cohesion principles.

Modules are lowly coupled when they adhere to component coupling principles.

Component Cohesion Principles

SOLID principles are the foundation of clean architecture and can be adapted at the module level, thus creating a new set of principles (REP, CCP, and CRP).

The Common Closure Principle (CCP)

“Gather into components those classes that change for the same reasons and at the same times.

Separate into different components those classes that change at different times and for different reasons.” — All remaining quotes by Robert C. Martin, Clean Architecture

The CCP is the evolution of the SRP at the module level, as I explained in my previous article.

A class should not change for different reasons -> a component should not change for different reasons.

Classes that change for the same reasons should be grouped in a component, and classes that change for different reasons should be moved away from the component.

Maintainability is more important than reusability: whenever you work on a new feature, or there is a requirement change, you would rather have to touch a single module than many.

When we have to change only a single module, we are less likely to affect other team members, and we have fewer components to recompile, revalidate, and redeploy.

It is not realistic to always group every possible change in a single module (unless you work with a monolith 😈), so the goal of the principle is to minimize the number of modules that needs changing.

Pros: Optimal for maintenance as the impact of change is minimized.
Cons: The optimal approach for developing and maintaining the modules might not be optimal for releasing the modules to library users. Also, modules will tend to be bigger to isolate the number of modules to change.

The Common Reuse Principle (CRP)

“Don’t force users of a component to depend on things they don’t need.”

The CRP is the evolution of the ISP at the module level, as I explained in my previous article.

When interfaces are small, you don’t depend on methods you don’t need -> when modules are small, you don’t depend on files you don’t need.

Classes are seldom reused in isolation. More typically, reusable classes collaborate with other classes that are part of the reusable abstraction.
The CRP states that these classes belong together in the same component.

It also states that classes not reused together should not be placed in the same component.

By doing so, an update on such classes won’t trigger a recompilation, redeployment, or release of modules that do not use them.

Pros: Smaller modules, as a module user you are less likely to be affected by changes you don’t care about.

Cons: More modules that require to be worked on during development.

The Reuse/Release Equivalency Principle (REP)

“The granule of reuse is the granule of release.”

The smallest thing you are willing to reuse is the smallest thing you are willing to release.

This is a very important principle for library devs.

Whenever you want to make a component available to others, you are required to have a release process, and for your component to not break your library users’ code over time, you are required to have release numbers.

By doing so, library users won’t have breaking changes unless they upgrade to the newer library versions.

Because all the classes in your module have the same release number, an update of a single class will require a new release of all the classes under the same module.

Sometimes, libraries come as a single library, and sometimes they come as a group of libraries (so you can decide what to import and what to exclude).

When working with a group of libraries, you may expect that because all these modules are reused together, they should all have the same release number to ensure compatibility.

Having the same release number means that when you need to update one module, you’ll also need to release all the other modules with the updated version number (even when these have not changed).

Let’s take Retrofit as an example.

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

When Retrofit developers add new functionalities to the Retrofit main library, they most likely also need to make the supporting converter libraries compatible with these new integrations, thus bumping the version name of all the library modules.

This might not always be the best approach, in particular when modules of the same group of libraries are not very cohesive.

Let’s now take Firebase as an example.

Firebase libraries used to have matching release numbers in the past.

The problem with Firebase is that their set of libraries is very incohesive.
Think about the remote config library and the storage library: these two libraries are completely independent of each other and probably worked on by different dev teams.

A new integration in one of the two should not require the Firebase team to release a new version of the other library without changes.

What did the Firebase team end up doing?

They took advantage of Gradle 5.0 support for Maven BoMs, which allows managing different library versions as a single version.

By doing so, instead of releasing a new version of every single library, they release a new version of the BoM.

 // BoM
implementation platform('com.google.firebase:firebase-bom:$version')

// modules import without version
implementation 'com.google.firebase:firebase-core'
implementation 'com.google.firebase:firebase-config'
implementation 'com.google.firebase:firebase-storage'

If it weren’t for BoMs, they would have used Google Play Services’ approach, aka an endless table of library versions (the least user-friendly).

Pros: Optimal for reusability, your modules are available to other teams, and the versioning makes new updates easier to manage.

Cons: It is more complicated to maintain the codebase as now you need to consider the module's release process.

Modules will tend to be bigger because by doing so, you can decrease the number of modules that need releasing.

The Component Cohesion Tension Triangle

REP (group for reusers) to CCP (group for maintenance) — too many unneeded releases REP (group for reusers) to CCP (group for maintenance) — too many unneeded releases CCP (group for maintenance) to CRP (split to avoid unneeded recompilations/releases) — hard to reuse CRP (split to avoid unneeded recompilations/releases) to REP (group for reusers) — too many components changed caption: what is your right fit?
Component Cohesion Tension Diagram

The diagram shows what happens when you abandon one principle in favor of the other two.

If it wasn’t clear already, the Component Cohesion Principles, unlike SOLID principles, don’t complement each other and require you to choose what is more important for your project.

While it is easy to make classes easy to maintain and reuse, the same cannot be said for modules.

The CCP and the REP are inclusive principles; they tend to make modules bigger, while the CRP is an exclusive principle as it tends to make modules smaller.

The CRP and the REP are principles focused on reuse. They tend to optimize the modules for those who are using them, while the CCP is focused on maintenance as it tends to optimize modules for those who are developing them.

It is very unlikely that you’ll manage to balance all three, so you should be prepared to give up or focus less on one of them.

Usually, a project falls under one of the following categories:

  1. App: when you are building an app, your main goal is to build things fast and have a project that compiles quickly with minimal unneeded recompilations.
    If you fall under this category, you should always focus on CCP and CRP.
  2. Library: when you are building a library, your goal won’t be static. On the contrary, it will change over time.
    When you start the development of the library, your main focus should be on building the library fast. Then, over time, your focus should shift to how reusable the library is and compromise its maintenance.
    If you fall under this category, you should focus on the right side of the triangle until your project is mature, then you should move to the left side over time as now you’ll be more and more accountable to your library users.

Most projects’ modularization fails because engineers prioritize the wrong principle for their projects’ nature.

Other projects’ modularization fails because the component structure is static instead of evolving as requirements change.

References

* Clean Architecture, Chapter 13 (Component Cohesion)

Components Coupling Principles

We discussed the theory about how modules should be according to the Cohesion Principles.

We now need to discuss how the relationships between these modules should be.

The Acyclic Dependencies Principle (ADP)

Example of Cyclic Dependency Principle

“Allow no cycles in the components dependency graph.”

If A depends on B, then B should not depend on A.

This is not only true for dependencies but also for transitive dependencies:
If A depends on B and B depends on C, then C should not depend on A either.

Some compilers allow for cycles in modules, and others try to ensure this doesn’t happen.

Regardless of the compiler or language used, you, as a developer, need to know how to break the dependency cycle as soon as you detect it.

A dependency cycle can be broken in two ways:

  1. Extracting the classes to reuse in a new module.
  2. Using the Dependency Inversion Principle (DIP, yes, SOLID again 😈) to “invert” the dependency.

Solution 1 is ideal when many modules require the shared logic and when a lot needs sharing.

Solution 2 is ideal when only a single module requires the shared logic and when there is not much to share.

The Stable Dependencies Principle (SDP)

“Depend in the direction of stability.”

What would you make your module rather depend on?
A module that changes often or a module that never changes?

We want our modules to depend on modules that never change.

Whenever a dependency in our module changes, our module needs to recompile, and we may have to deal with breaking changes.

Which modules are stable?

Stable modules are the ones that are hard to change.

Think about the Kotlin String class, how likely is the Kotlin team to change such a class? If they do change it, how many breaking changes would they have in the whole Kotlin language? That is a class that no developer sane of mind would ever change.

The ideal scenario for you is that your module depends on something as stable as that.

Unfortunately, we live in the real world, not the ideal one.

Most modules we use are not 100% stable, which is not necessarily bad.

A module that cannot change cannot also ever improve.

Not only that, if modules cannot change at all, we can never add new features as we can’t change the code.

So, how do we redefine stability?
When is a module stable enough?

A module is stable when it has few dependencies and many dependent modules, thus making it a responsible module.

If you look at your components dependency graph, you should see at the bottom the more stable (responsible) modules and at the top the more unstable (dependent) modules.

Because in your project, you’ll end up having both stable and unstable modules, the golden rule is that a module should depend on modules that are more stable than itself.

The Stable Abstractions Principle (SAP)

“A component should be as abstract as it is stable.”

The SDP defines that stable modules are hard to “change.”
This implies that adding new functionalities to stable modules is hard as you cannot easily modify the existing code…

But what about “extension”? Can I extend stable modules?

The Open-Closed Principle (OCP, yes SOLID again 😈) gives us classes that are open to extension and closed to modification.

How do I port this extensibility at the module level?

A module is easy to extend when it is abstract. Hence it is composed mainly of interfaces and abstract classes.

When a module is full of interfaces, whenever you need to add something new, all you need to do is to provide a new concrete implementation to one of the abstractions.

This will prevent you from touching the stable modules’ source code just to adapt something for your module and potentially break the other dependent modules.

While stable modules should be more abstract than concrete to allow more flexibility, unstable modules should be more concrete than abstract to facilitate code changes.

Although you want stable modules to be very abstract to allow flexibility, a module that is 100% abstract is a module that is useless as there is no actual logic that you can reuse.

And obviously, a stable module that is 100% concrete is a module that is very painful to change.

The golden rule here is that a module should depend on the abstractions of its dependencies rather than the concretion.

You should be getting this for free if your classes adhere to the Dependency Inversion Principle.

References

* Clean Architecture, Chapter 14 (Component Coupling)

Package Design Solutions

If you got bored reading the above six principles, worry not, as now I’m going to get into the fun part.

Now that we know how to achieve high cohesion and low coupling, it is time to discuss which approaches work and which don’t.

Because app developers are the ones with the most trouble modularizing and because library developers don’t usually have to deal with a lot of modules, I will only focus on how to modularize an app (or this article will become even longer!).

Package by layer

Package by Layer Diagram

In package by layer, you split the codebase into three broad modules, one for each layer. — Author

This approach is quite easy to do but violates most of the abovementioned principles.

You’ll most likely have to modify all the modules whenever you work on a new feature.

By doing so, you may end up breaking other features' code, stepping on your teammates' feet, and recompiling the whole dependency graph at any new iteration.

Modules will also be very big, as they will contain the layer logic of all the features in your app.

Why is this approach so popular?

If you’ve been reading Clean Architecture articles since the beginning of the hype, you would have noticed how most writers, when discussing modularization, constantly pushed for the Package by Layer approach, thinking that layers (Presentation-Domain-Data) should dictate the modules’ structure of their project.

If these writers had read the Clean Architecture book in the first place, they would have known that this approach was the one most advertised against.

The reason why this bad advice makes me upset is not just because devs used the wrong modularization approach as a consequence but also because it lead companies astray as they separated development teams by these layers.

What if I want to swap a database for another database?
Isn’t it better to have to change a single module?

I’ve heard this many times in support of this packaging approach, and the short answer is no, it is not better.

Firstly, it is not part of your day-to-day work to change your database. This may happen over the years but definitely not on a weekly basis.

Not to mention that this is very rare work for mobile developers (some unlucky devs had to change Sqlite with Realm and then back to Sqlite with Room, but this happened over many years).

Secondly, it is a bad idea to swap a database in a single blow. It is much better to migrate feature by feature your data to the new database so that you can gradually release your migration and limit the number of potential bugs that may come out.

Package by feature

Package by Feature Diagram

In package by feature, you split the codebase into feature modules, one for each feature. — Author

This approach brings many advantages, and it has been the most recommended approach for decades:

  • When working on a feature, you change only one module, which is optimal for maintenance.
  • When you open your project, you know exactly what your project does as it screams at you what it is about (Screaming Architecture).
  • Each cross-functional team can work independently on a feature without stepping on another team’s feet.
  • Independent teams also mean independent modules, so you can get the most out of Gradle parallel compilation, which will decrease your overall compilation time other than requiring you to recompile only that single feature that changed.
  • You don’t lose layers, as layers can easily be implemented as packages inside the feature module.

So this is how I should modularize my app, right?

Not really. This approach looks optimal for maintenance but has no reusability at all!

The cons of this approach are very expensive:

  • If your feature module needs to reuse some code from another feature module, you’ll need to create a dependency on a very unstable module which would break the SDP, also because features will contain a lot of UI code, and UI code is very concrete; you’ll also break the SAP.
  • Feature modules contain presentation, domain, and data logic which results in big modules (CRP violation) containing code that often changes (UI) along with code that rarely changes (business logic).
  • Features depending on features often create big “Core Features” modules that revert your project gradually to a monolith.

Package by feature works well in non-UI-heavy projects like backend or old frontend applications.

In a backend project, the controllers' code (presentation layer) is usually thin and matches the use cases or services of the domain layer.

The backend can also rely on services (or microservices) rather than modules, so communication within the components does not require a service to know the internal structure of another service. Instead, the public API’s contract makes the whole components’ structure independent at compilation time.

In mobile or web frontend projects, “screens” are clusters of features.

Think of an e-commerce product detail page that allows you to add the product to the cart and the user’s wishlist.

These same actions can be performed on a product list page or the cart page, or the wishlist page.

How can you split by feature in this case?
Are you going to group everything under a single feature?
Are you going to create a big shared feature module for sharing common code?
Are you going to duplicate a lot of your code so you can have independent modules?

Any of these solutions are suboptimal and are not the answer to the problem.

Package by feature is not the right fit for UI-heavy projects, so don’t use it on professional Android projects (the same applies to iOS and Web).

Package by component

Package by Component Diagram of the PDP scenario

In package by component, you split the codebase into UI modules and component modules (domain + data layer of a feature). — Author

Who should guide the modules of your app?

Certainly not the UI logic as we saw in package by feature.
Use cases should guide your modularization just like they guide your development.

Use cases tell us what the app does and rarely change. They are also the only architectural component visible by the presentation layer.

The data layer exists only to support the domain layer. Hence a modification of the domain layer will often require a modification in the data layer to be compatible with the updated repository interface.

By splitting the codebase vertically and horizontally using use cases, we can achieve the reusability we didn’t have in package by feature.

Going back to the product detail page example (check the diagram). If I have a Cart Component module, a Wishlist Component module, and a PDP UI module, I can now reuse both cart and wishlist codes without depending on any UI detail for showing the cart or the wishlist screens.

And if the product team decides to introduce the add-to-cart functionality in the wishlist screen, we can just add the Cart Component module as a dependency to the Wishlist UI module and link it.

Not only do I now have a more reusable approach, but I also have separated classes that often change from classes that rarely change, thus minimizing the number of recompilations.

Because components are still independent, we can compile modules in parallel, thus improving the overall compilation time.

What if I need to share code between component modules or UI modules?

You will most likely encounter that problem if you work on a professional project, and the solution is as follows:

If what you need to share is a feature’s specific code, you can extract what you need to reuse in a Shared Component module or a Shared UI module.

If what you need to share is generic code, let’s say code for performing a network request or a design system, you would follow the same approach you always use with third-party libraries (like Retrofit, Dagger…) with the difference that this module won’t be public but private for your project (until you decide to share it with the public).

What if I need to navigate from the PDP to the Cart screen or the Wishlist screen?

DIP is your friend. If you need to navigate through modules, all you need to do is to have an interface, let’s say:

interface PDPNavigator {
// you can adapt for fragments, navigation component, compose....
fun navigateToCart(activity: Activity)
fun navigateToWishlist(activity: Activity)
}

Which will be implemented by a class in the Main (app) module:

class AppNavigator: PDPNavigator, WishlistNavigator, CartNavigator.... {
override fun navigateToCart(activity: Activity) {
//...
}

override fun navigateToWishlist(activity: Activity) {
//...
}

}

You don’t need to do this for screens inside the same module.

References

* Clean Architecture, Chapter 34 (The Missing Chapter) by Simon Brown
* Simon Brown’s original post
* Martin Fowler’s referenced article in The Missing Chapter

Encapsulation

If there is one thing developers never do well, it is working with encapsulation.

Open one of your module's code. If every single class or interface is public, you are doing encapsulation wrong.

“Public” is a modifier that should be used only for those classes or interfaces meant to be used outside the module. Everything else should be “internal.”

If every class or interface is “public” inside a module, a developer might be misled into thinking that everything needs to be reused by other modules and will be afraid to touch the code.

More disciplined developers, to gain confidence, would use the IDE find usages tool to check that these classes are used outside of the module.
If only there were a modifier that could have avoided these extra steps and made devs more productive.

Module encapsulation is not just about confidence but also about future improvements.

By knowing what is public and what is not, you may find a way to decouple a module from a dependency in cases where you only use it because of a small number of classes/interfaces.

It doesn’t end with public/internal modifiers.

Another encapsulation problem you may find is related to the exposure of transitive dependencies.

This happens when one of your dependencies is leaking a transitive dependency by using api instead of implementation.

Ideally, you always use implementation, as this avoids the leaking of dependencies and extra compilation time.

Encapsulation with package by component

In Package by Component, it is quite simple to work with encapsulation as the number of files that can be public is just a few.

  • In component modules: the only files that should be public are the use-case interfaces and the models that need to be used outside the module.
    Use case implementations, repository interfaces, repository implementations, mappers interfaces, mappers implementations, DTOs, etc., should always be internal, as the presentation layer should not access them.
    Component modules are stable (SDP adherence), containing business rules and use cases. By making only use case interfaces public, we also adhere to the SAP since now other modules will only depend on the abstraction of this module.
  • In UI modules: the only files that should be public are the screens (fragments, activities, compose screen Composables) and the external navigators.
    UI modules are unstable since UI is very volatile; hence, we should try not to add them as dependencies and import them only in the Main (app) module to connect the navigation.

If you are working with Dagger, the modules providing these dependencies should also be made internal.

// Dagger module for a Wishlist Component Module
@Module
@InstallIn(SingletonComponent::class) // Or any other scope
internal object WishlistComponentModule {

@Provides
fun provideAddToWishlistUseCase(
addToWishlistUseCaseImpl: AddToWishlistUseCaseImpl
): AddToWishlistUseCase = addToWishlistUseCaseImpl

@Provides
fun provideGetWishlistUseCase(
getWishlistUseCaseImpl: GetWishlistUseCaseImpl
): GetWishlistUseCase = getWishlistUseCaseImpl

@Provides
fun provideWishlistRepository(
wishlistRepositoryImpl: WishlistRepositoryImpl
): WishlistRepository = wishlistRepositoryImpl

//...

}

// Dagger module for a Wishlist UI Module
@Module
@InstallIn(ActivityComponent::class) // Or any other scope
internal object WishlistUIModule {

@Provides
fun provideSomeDependency(
someDependencyImpl: SomeDependencyImpl
): SomeDependency = someDependencyImpl

//...

}

If you are using manual injection, you can make your Dependency Containers public and make sure that only public files are returned from a public method (you will get a compilation error otherwise).

References

* Gradle API vs implementation documentation

The Main Component

In every system, at least one component creates, coordinates, and oversees the others.

The Main Component (app module in Android) is the ultimate detail, it contains the lowest-level policies and is the system’s entry point.
It is a dirty low-level module in the outermost circle of the clean architecture. It loads everything up for the high-level system and then hands control over to it.

The following should be placed in this area:

  • all the “glue code” required to connect the modules
  • all the injection code that cannot be internal in the modules
  • all the navigation code that cannot be internal in the modules
  • your flavors configurations
  • all the initialization required by the frameworks

References

* Clean Architecture, Chapter 26 (The Main Component)

Last Notes

I know that the article was long and that principles are never fun, especially when they require you to know other principles beforehand.
If I had to write this article with only the solutions, these would have just been an opinion to the reader rather than the product of the component principles.

Despite this being a long read, a lot has been left out, like Domain-Driven Design concepts for decoupling the contexts or Package by Ports and Adapters (another suboptimal package design solution).

I hope the article was still enjoyable despite the theoretical parts and you have learned new tools to modularize your projects.

--

--