Modularization splits an Android project into multiple Gradle modules. Each module has a defined responsibility and explicit dependencies. Large apps at companies like Google, Spotify, and Airbnb are highly modularized.
The benefits are real but the cost is also real. Start with a clear reason.
Why Modularize
Build time. Gradle only recompiles changed modules. In a monolith, a one-line change can trigger a full recompilation. With modules, only the affected module and those that depend on it rebuild.
Ownership. Teams can own specific modules. Changes are isolated.
Enforced boundaries. A module cannot access internal classes from another module unless they are explicitly exposed. This replaces informal rules with compile-time enforcement.
Dynamic delivery. Feature modules can be downloaded on demand with Play Feature Delivery, reducing initial APK size.
Layer-Based Modularization
The simplest approach is one module per architectural layer:
:app (wires everything)
:presentation (ViewModels, UI state)
:domain (use cases, models, repository interfaces)
:data (repository implementations, Room, Retrofit)
This works for small teams but does not scale. The :data module becomes a monolith containing all remote and local sources.
Feature-Based Modularization
Organize by feature, not by layer:
:app
:feature:profile
:feature:feed
:feature:settings
:core:ui (shared composables, themes)
:core:network (Retrofit setup, interceptors)
:core:database (Room setup, shared DAOs)
:core:domain (shared models, base use cases)
Each feature module is independent and owns its layers internally. Core modules provide shared infrastructure.
Hybrid Approach (Recommended)
Most production apps combine both:
:app
:feature:profile
├── :feature:profile:ui
├── :feature:profile:domain
└── :feature:profile:data
:core:ui
:core:network
:core:testing
This gives strong boundaries inside large features while keeping shared infrastructure in core modules.
Dependency Graph Rules
The dependency graph must be acyclic. Never let a lower-level module depend on a higher-level one.
:feature modules → :core modules
:app → :feature modules + :core modules
:core:domain → nothing (only pure Kotlin)
Enforce this with a Gradle plugin or lint rules. Circular dependencies cause build failures and are hard to untangle later.
Module Types in Gradle
// feature module — Android library
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
// core:domain — pure Kotlin, fastest to compile
plugins {
id("org.jetbrains.kotlin.jvm")
}
Pure Kotlin modules (:core:domain) compile faster than Android library modules because they skip the Android toolchain.
Convention Plugins
Repeating the same Gradle configuration across 20 modules is noise. Use convention plugins to centralize common setup.
// build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt
class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.android.library")
pluginManager.apply("org.jetbrains.kotlin.android")
extensions.configure<LibraryExtension> {
compileSdk = 35
defaultConfig.minSdk = 24
}
}
}
}
Then each feature module just applies one plugin:
plugins {
id("convention.android.feature")
}
Common Pitfalls
| Pitfall | Fix |
|---|---|
| Too many modules too early | Start with 5-10 modules, split as the team grows |
| No shared navigation contract | Define navigation routes in :core:navigation |
| Feature modules depending on each other | Route through :app using a navigation abstraction |
Putting everything in :core | Core should be truly shared; feature logic stays in features |
Practice
Take a single-module project. Extract a :core:domain module with data classes and repository interfaces. Move no implementations — just the contracts. Then verify that the :app module compiles with the new structure.
Summary
Modularization enforces boundaries, speeds up builds, and scales to large teams. Feature-based organization with shared core modules is the industry default. Start small, use convention plugins to reduce boilerplate, and enforce a strict dependency graph.