When does a single module become a problem?
Module Types and the Dependency Graph
| Type | What the guide says |
|---|---|
| Data | "A data module usually contains a repository, data sources and model classes." |
| Feature | "Feature modules depend on data modules." |
| App | "App modules are an entry point to the application. They depend on feature modules and usually provide root navigation." |
| Common | "Common modules, also known as core modules, contain code that other modules frequently use." |
// settings.gradle.kts
include(":app")
// Feature modules: screens and their ViewModels
include(":feature:home")
include(":feature:search")
include(":feature:checkout")
// Data modules: one per domain, each exposing a repository
include(":data:books")
include(":data:orders")
// Common modules
include(":core:model")
include(":core:ui")
include(":core:network")
include(":core:testing")Data Modules and the Model
:core:model module and called that module the glue between features. That is a workable design, but it is not what the guide describes, and it pushes every contract in the app into one module that everything depends on. A change to any of them recompiles everything. Keep the repository with its data module. Keep :core:model for the plain types that cross module boundaries.
Those plain types do not need Android. The guide's advice on module kind is direct: "Since Android modules come with overhead, preferably, you'd want to use the Kotlin or Java kind as much as possible." A model module is the easiest place to follow it.// core/model/build.gradle.kts: a plain Kotlin module
plugins {
kotlin("jvm")
}
// :core:model
data class Book(
val id: String,
val title: String,
val priceInCents: Long,
)
// :data:books, the module's public API
interface BookRepository {
suspend fun getBook(id: String): Book
fun observeBooks(): Flow<List<Book>>
}
// :data:books, hidden from every other module
internal class NetworkBookDataSource(/* ... */)
internal class DefaultBookRepository(
private val network: NetworkBookDataSource,
) : BookRepository {
override suspend fun getBook(id: String): Book = TODO()
override fun observeBooks(): Flow<List<Book>> = TODO()
}Feature Module Structure
internal on a ViewModel means no other module can touch it.
The same restraint applies to dependencies. "When declaring dependencies in your module, prefer implementation over api." What that does to build time is in the Gradle build optimization guide.// feature/checkout/
// build.gradle.kts
// src/main/kotlin/com/app/feature/checkout/
// CheckoutScreen.kt public: the entry point
// CheckoutViewModel.kt internal
// CheckoutUiState.kt internal
// CheckoutScreen.kt: the only public declaration
@Composable
fun CheckoutScreen(
onOrderPlaced: (orderId: String) -> Unit,
) {
// ...
}
internal class CheckoutViewModel(/* ... */) : ViewModel()
// feature/checkout/build.gradle.kts
dependencies {
implementation(project(":data:books"))
implementation(project(":data:orders"))
implementation(project(":core:model"))
implementation(project(":core:ui"))
// No other feature module appears here
testImplementation(project(":core:testing"))
}Build Time: What Modules Buy and What They Cost
buildSrc that applied the kotlin-android plugin. AGP 9 rejects that plugin, so the sample would fail today.// feature/checkout/build.gradle.kts, with a convention plugin
plugins {
// compileSdk, minSdk and Java level are set once, here
id("android-library-conventions")
}
android {
namespace = "com.app.feature.checkout"
}Navigation Across Module Boundaries
// :feature:home knows nothing about checkout
@Composable
fun HomeScreen(
onBookSelected: (bookId: String) -> Unit,
) {
// ...
}
// :app owns the navigation graph, so it is the mediator
HomeScreen(
onBookSelected = { bookId ->
navController.navigate("checkout/$bookId")
}
)
// :feature:checkout reads the id and loads the book itself
internal class CheckoutViewModel(
savedStateHandle: SavedStateHandle,
bookRepository: BookRepository,
) : ViewModel() {
val bookId: StateFlow<String> =
savedStateHandle.getStateFlow<String>("bookId", "")
}Key Takeaways
- 1Modularize when the codebase is large enough to need it. Android says it does not always make sense.
- 2Four module types: data, feature, app and common. Feature modules depend on data modules, and the app module depends on features.
- 3A data module exposes a repository and hides its data sources. Keep the repository with its data, not in a shared model module.
- 4Mark everything internal except the entry point, and prefer implementation over api.
- 5Modules can improve build time through incremental build, the build cache and parallel build. They also add configuration overhead, which convention plugins contain.
- 6Features talk through a mediator, usually the app module, and pass ids, not objects.
Frequently Asked
When should I modularize my Android app?
Android gives a clear test. If you need reusability across apps, strict visibility control or Play Feature Delivery, modularization is a necessity, because those benefits are only achievable with modules. If you want better scalability, ownership, encapsulation or build times, it is worth considering. If the codebase will stay small, the gains do not apply.
How many modules is too many?
There is no number. Android describes two failure modes: too fine-grained, where every module adds build complexity and boilerplate until the overhead outweighs the benefit, and too coarse-grained, where a module grows into another monolith. Start with clear boundaries, and consolidate modules when the configuration cost exceeds what the split gives you.
Can feature modules depend on each other?
Not in both directions: Gradle does not allow cyclic dependencies between modules. We forbid it in either direction as a convention. Android recommends a mediator instead, usually the app module that owns the navigation graph, which passes a simple id from one feature to the next so that neither knows about the other.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.