Kotlin Multimodule Android Architecture for Large-Scale Applications
As an Android application grows, a single Gradle module becomes harder to maintain. Build times increase, dependencies become tightly coupled, and unrelated features can accidentally depend on each other.
A multimodule architecture divides the application into independently managed modules while enforcing clear dependency boundaries.
Architecture
A large application can be organized like this:
app
├── core
│ ├── common
│ ├── network
│ ├── database
│ └── ui
├── domain
│ ├── auth
│ ├── orders
│ └── payments
└── feature
├── auth
├── profile
├── orders
└── payments
Feature-Based Modules
Prefer business capabilities over extremely granular technical modules:
feature:auth
feature:orders
feature:profile
feature:payments
A feature can internally follow Clean Architecture:
feature:orders
├── presentation
├── domain
└── data
Gradle Configuration
Declare modules in settings.gradle.kts:
include(
":app",
":core:common",
":core:network",
":feature:auth",
":feature:orders"
)
A feature can consume shared functionality:
dependencies {
implementation(project(":core:common"))
implementation(project(":core:network"))
}
Prefer implementation over api unless a dependency must be exposed to consumers.
Dependency Direction
A healthy dependency graph might be:
app
↓
features
↓
domain
↓
core
Avoid direct feature-to-feature dependencies:
feature:orders → feature:profile
If two features need the same contract, move that contract to an appropriate shared module.
Dependency Inversion
Suppose orders needs user information. Define an abstraction:
interface UserProvider {
suspend fun getUser(): User
}
The orders feature depends on the interface instead of the profile implementation.
Orders
↓
UserProvider
↑
Profile implementation
This keeps modules loosely coupled.
Core Modules
Useful shared modules include:
core:common
core:network
core:database
core:designsystem
Avoid creating one enormous core module containing every shared dependency. A giant core module eventually becomes another monolith.
Build Performance
Multimodule projects can improve incremental compilation because changes are isolated.
Measure:
- Kotlin compilation time
- Android compilation time
- Annotation/KSP processing
- Configuration time
- Dependency graph size
Too many poorly designed modules can increase build complexity, so module boundaries should represent meaningful ownership or architecture.
Testing
Each module can have independent tests:
feature:auth
├── unit tests
├── repository tests
└── UI tests
This makes it easier to validate features without always building the entire application.
Convention Plugins
Large repositories often repeat Gradle configuration. Convention plugins can centralize common settings such as Android, Kotlin, Compose, testing, and publishing configuration.
A module can then use:
plugins {
id("company.android.feature")
}
instead of duplicating configuration everywhere.
Common Mistakes
Avoid:
- One giant core module
- Feature-to-feature dependency chains
- Excessive module creation
- Exposing dependencies unnecessarily with
api - Putting business logic into build configuration
Conclusion
Multimodule architecture is not about creating the maximum number of modules. It is about creating boundaries that allow teams to build, test, and change parts of an application independently.
For large Kotlin Android applications, combining feature modules, dependency inversion, focused core modules, and convention plugins provides a scalable foundation.
Useful Links
SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter
SDK Android: https://github.com/v-modal/vmodal_sdk_android
Discord: https://discord.gg/K72z28KUx
Top comments (0)