The Android app modularization

Transforming Coupang’s legacy architecture for Android — part 2

Coupang Engineering
Coupang Engineering Blog
10 min readOct 30, 2018

--

By Seongchul Park

Series index

This is part 2 of a series on “Transforming Coupang’s legacy architecture for Android.”

Illustration of modules

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.core2
settings.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.

Example illustration of the core modules and its interface
Figure 1. Example of the core modules and its interface

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-feature2
settings.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%.

Basic representation of Coupang’s modularization architecture
Figure 2. Basic modularization architecture of Coupang

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!

--

--

Coupang Engineering
Coupang Engineering Blog

We write about how our engineers build Coupang’s e-commerce, food delivery, streaming services and beyond.