Modularization: The right way.

Kshitij Jain
3 min readOct 22, 2021

--

Modularization is the activity breaking monolithic codebase into several independent small sub-parts called modules, and it is a recommended practice to scale software projects.

There are many blogs regarding what is modularization and why should we do it, but I found very few of them covering about abstraction modules.

Before moving to what are abstraction modules, let’s see what are the problems with traditional modularization approach?

Modules usually have independent logic/features in them but using them directly in other modules, this leads to multiple problems:

  1. Modules are tightly coupled to each other as they use specific logic/feature implementation directly.
  2. Decrease in code testability. When we rely directly on implementation of logic/feature, whenever there is some change in logic, tests needs to be updated for the code which is accessing it.
  3. If you have done modularization before, you must be aware of circular dependency issues. When a feature A depends on feature B and feature B depends on feature A, it will cause circular dependency issue.
  4. When a feature A depends on feature B implementation, change in feature B will cause feature A to recompile and this leads to increase in the build time of the application.

So, what are abstraction modules?

Abstraction modules are modules which acts as bridge between implementation modules. They just contains interfaces and data classes. Instead of logic/features directly depending on implementation of each other, they will rely on interfaces (abstraction modules).

On the top of solving above issues, there are many benefits of using abstraction modules:

  1. Isolating third party libraries. Example, what if we are using Picasso image loading library and we want to migrate to Glide. If we are using abstraction modules, we just have to change implementation (no change in other logic/features).
  2. Different implementations for debug/release build. Example, in debug app we want to print logs in console but in release build we want to push logs to server.
  3. Fail-safe for third party libraries. Example, there is a crash happening in app due to third party library in production and we can disable third party library easily.

Let’s dive in!

1. For every feature/logic module, we will create an abstraction module. I named them as api modules in sample code.

Project Structure: https://github.com/KshitijDroid/Modularisation-Demo

2. Abstraction modules will have interfaces and data classes. In sample app, feature B navigates to feature A. So let’s create interface in featureA-api module for it.

Abstraction module structure
Interface to expose logic/feature

3. We need to connect interfaces with their implementation. Application module will be used for it. I have used dagger (dependency injection framework) to make connecting interfaces and implementation easier. (I will write separate blog post for the same.)

Using dagger to connect interface with implementation.

4. Now, featureB module will depend on featureA-api module (as Feature B wants to navigate to Feature A).

Adding dependency in build.gradle.
Dependency graph

5. Finally, FeatureB will consume interface provided by featureA-api module to navigate to FeatureA.

Consuming interface from abstraction module.

References:

  1. https://github.com/KshitijDroid/Modularisation-Demo
  2. https://jeroenmols.com/blog/2019/03/06/modularizationwhy/
  3. https://medium.com/google-developer-experts/modularizing-android-applications-9e2d18f244a0
  4. https://dagger.dev/

--

--

Kshitij Jain

Avid android engineer, tech geek, and big time foodie. Software Engineer at Booking.com