The Android app modularization
Transforming Coupang’s legacy architecture for Android — part 2
By Seongchul Park
Series index
This is part 2 of a series on “Transforming Coupang’s legacy architecture for Android.”
- Part 1 — Concerned? Separate the concerns!
- Part 2 — The Android app modularization
- Part 3 — Reducing dependencies through repackaging
This post is also available in Korean.
In the previous post, we look at how we achieved separation of concerns by applying the MVP pattern. While this helped us to improve test coverage and publish a more stable and reliable release, we were still conducting too many tasks on a single module and experiencing long build times.
Also, things had changed for Coupang. We made significant progress as an e-commerce app and were expanding our business to new sectors. Naturally, we wanted reuse the code that is proven to be safe and reliable so that the developers for the new projects can focus on building the new parts of the service. We also wanted to take advantage of the new distribution features like instant apps and app bundles from Google. In this post, we will talk about the solution to our problems — modularization.
Our goals and approach
We had 3 main goals.
- Enable code reusability
- Improve app integrity and maintainability
- Reduce code dependency
Taking account of these goals, we broke down the monolithic codebase into role-specific modules.
App module
This is the root module of the Coupang app, which is declared in the build.gradle
file by the app plugin.
apply plugin: 'com.android.application'
The app module takes the role of binding all modules and defines the configurations of the app.
Feature modules
This is the module containing specific features, specialized to the relevant business domains of Coupang like Home and Search. We minimized the dependencies between the domains. It is declared in the build.gradle
file by the library plugin.
apply plugin: 'com.android.library'
Domains like Search can operate independently but domains like the Shopping Cart usually influences other domains. Thus, the feature module is designed to be able to serve as an independent module or a common module depending on how it is used.
Core modules
This is a decoupled module that does not have any dependency on the app and can be used by other apps. Like the feature module, it is declared in the build.gradle
file by the library plugin.
The core modules were designed to operate like a library. We allowed other classes to use the core module through configuration injection on an external file.
Modularization process
Splitting the codebase into modules in one go is not as easy as it sounds, especially when there is more than hundreds of thousands lines of code. We proceeded with modularization in a series of steps described below.
1. Separation of the core module
We started with the easiest option. We started splitting features that had been proven reliable as core modules. During this process, we also removed any dependencies to the businesses to enable a feature-specific module.
package:
com.yourcompany.core1
com.yourcompany.core2settings.gradle:
include ':core1'
include ':core2'build.gradle:
compile project('core1') // or implementation
compile project('core2')
We created the core modules while considering the following factors.
- Reducing dependency to minimize clashes (gradle extra) between modules.
- Solving override or merging issues caused from bad relationships.
- Applying the existing lint rules to check operations and performance.
- Checking any duplicate ProGuard rule configurations.
Core modules at times is managing the use of third-party libraries. Thus to prevent misuse and to minimize the effect on the application module during library upgrades or changes, we limited any direct implementation by other classes.
After numerous code reviews and QA tests, we were able to create one app module and 13 core modules.
2. Separation of the feature module
After successful separation of the core module, we started to split each of the independent feature modules and combined all elements like resources with dependencies to the common feature module.
package:
com.yourcompany.projectname-common
com.yourcompany.projectname-feature1
com.yourcompany.projectname-feature2settings.gradle:
include ':projectname-common'
include ':projectname-feature1'
include ':projectname-feature2'build.gradle:
compile project(':projectname-common') // or implementation
compile project(':projectname-feature1')
compile project(':projectname-feature2')
Conclusion
This is an ongoing effort. We are still continuing to split independent feature modules and have much more to do for app modules. However, we have created 13 core modules thanks to the months of collaborative efforts by many teams. And we maintain the unit test coverage of each module at 80%.
The process of modularization has not only increased stability and maintainability, but it allowed for efficient reuse of codes within the entire company for maximum productivity. In the next post, we will summarize how we proceeded with reducing dependencies by repackaging.
We’re actively looking for passionate individuals who are not afraid to ask questions, challenge the norm, and make things happen. Check out our open positions or sign up for job alerts!