On this page
A single :app module is the right start for almost every project. As the app grows to dozens of screens and several developers, one module means every change recompiles everything, any class can depend on any other, and ownership is blurry. Modularization addresses those problems, at a price.
What modules give you
Benefits
- Build speed: unchanged modules come from the cache, and builds can parallelise
- Enforced boundaries: the compiler blocks forbidden dependencies
- Encapsulation:
internalhides implementation details - Ownership: teams own modules
- Reuse: shared design system and core modules
- Dynamic delivery: optional on-demand feature modules
Costs
- More Gradle configuration to maintain
- Slower Gradle sync with many modules
- Cross-feature navigation needs indirection
- Visibility decisions (
apivsimplementation,internal) - Over-modularization makes simple changes touch many modules
A typical structure
api vs implementation
plugins { id("app.android.feature") } // convention plugin (see below)
dependencies {
implementation(project(":core:data")) // used internally: NOT exposed to consumers
implementation(project(":core:designsystem"))
api(project(":core:model")) // types appear in this module's public API
}Prefer implementation. It keeps dependencies from leaking to consumers and limits recompilation when they change. Use api only when a dependency's types appear in your module's public signatures.
Convention plugins: avoid copy-pasted build files
class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("com.android.library")
pluginManager.apply("org.jetbrains.kotlin.android")
pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
extensions.configure<LibraryExtension> {
compileSdk = 36
defaultConfig.minSdk = 24
buildFeatures.compose = true
}
dependencies {
add("implementation", project(":core:designsystem"))
add("implementation", project(":core:ui"))
}
}
}Each new feature module then needs only a few lines. Google's Now in Android sample uses this approach.
Navigating between features without coupling
If :feature:home needs to open search, it shouldn't depend on :feature:search. Common approaches:
- Route keys in a shared module (
:core:navigation), with each feature providing its entries, and:appassembling the graph. - Lambdas passed from
:app:HomeScreen(onOpenSearch = { backStack.add(Search) }).
Lessons learned
Modularize for a reason
Measure build times or identify ownership pain first. "Clean architecture says so" isn't enough.
Start with core extraction
Moving the design system, network and database into modules usually brings the biggest early gains.
Watch the graph
Visualise dependencies regularly. A "core:common" module that everything depends on becomes a bottleneck.
Granularity is a trade-off
Too few modules give little benefit. Too many give slow syncs and ceremony. Feature-sized modules are a good default.
When it pays off
| Situation | Recommendation |
|---|---|
| Solo developer, small app | Single module with clear packages |
| Growing app, slow incremental builds | Extract core modules, then the largest features |
| Several teams | Feature modules with owners; convention plugins; dependency rules in CI |
| Planning KMP | Pure-Kotlin domain and data modules that can become multiplatform |
Frequently asked questions
Does modularization always make builds faster?
Incremental builds and CI with caching usually improve, but configuration and sync times can grow with the number of modules. Measure before and after.
Should each layer be its own module?
Not necessarily. Many teams split by feature first, and add layer modules (domain, data) where boundaries need enforcing.
How do I wire DI across modules?
Each module contributes Hilt modules installed in components, and the app module brings them together automatically through Hilt's aggregation.
Conclusion
Multi-module architecture trades build configuration effort for faster incremental builds, enforced boundaries and team ownership. Start simple, extract core modules first, use implementation by default and convention plugins for consistency, and never let features depend on each other.
Next step: choose how to slice the app in Modularization Strategies for Large Android Apps.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Architecture makes the most sense when you apply it to a real project and see why each boundary exists. Learn how maintainable Android applications are designed by building one.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.