When a Monolith Becomes a Problem
Module Types and the Dependency Graph
// settings.gradle.kts
include(":app")
// Feature modules
include(":feature:auth")
include(":feature:home")
include(":feature:profile")
include(":feature:search")
include(":feature:checkout")
// Core modules
include(":core:network")
include(":core:database")
include(":core:ui")
include(":core:model")
include(":core:common")
include(":core:testing")The :core:model Module
// :core:model -- pure Kotlin, no Android dependencies
// build.gradle.kts
plugins {
id("org.jetbrains.kotlin.jvm")
id("kotlinx-serialization")
}
// Shared data types
@Serializable
data class User(
val id: String,
val displayName: String,
val email: String,
val avatarUrl: String?,
val createdAt: Instant,
)
// Repository interfaces -- implementations live in
// feature or core modules
interface UserRepository {
suspend fun getUser(id: String): User
fun observeUser(id: String): Flow<User>
suspend fun updateUser(user: User)
}Feature Module Structure
// :feature:profile module structure
// feature/profile/
// build.gradle.kts
// src/main/kotlin/com/app/feature/profile/
// ProfileNavigation.kt -- PUBLIC: navigation route
// ProfileScreen.kt -- internal composable
// ProfileViewModel.kt -- internal
// ProfileUiState.kt -- internal
// data/
// ProfileRepositoryImpl.kt -- internal
// di/
// ProfileModule.kt -- Hilt module
// ProfileNavigation.kt -- the only public API
fun NavGraphBuilder.profileScreen(
onNavigateToSettings: () -> Unit,
onNavigateToEditProfile: () -> Unit,
) {
composable(
route = "profile/{userId}"
) { backStackEntry ->
val userId = backStackEntry.arguments
?.getString("userId") ?: return@composable
ProfileScreen(
userId = userId,
onNavigateToSettings = onNavigateToSettings,
onNavigateToEditProfile = onNavigateToEditProfile,
)
}
}
// build.gradle.kts for :feature:profile
dependencies {
implementation(project(":core:model"))
implementation(project(":core:network"))
implementation(project(":core:ui"))
implementation(project(":core:common"))
// No feature-to-feature dependencies!
testImplementation(project(":core:testing"))
}Build Performance Gains
# gradle.properties -- optimize for multi-module builds
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
# Convention plugins: share build logic without copy-paste
# buildSrc/src/main/kotlin/AndroidFeaturePlugin.kt
class AndroidFeaturePlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
apply("com.google.dagger.hilt.android")
apply("com.google.devtools.ksp")
}
extensions.configure<LibraryExtension> {
compileSdk = 35
defaultConfig.minSdk = 24
buildFeatures.compose = true
}
dependencies {
add("implementation", project(":core:model"))
add("implementation", project(":core:ui"))
add("testImplementation",
project(":core:testing"))
}
}
}
}Navigation Across Module Boundaries
// :app module -- wires all feature navigation
@Composable
fun AppNavHost(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = "home"
) {
homeScreen(
onNavigateToProfile = { userId ->
navController.navigate("profile/$userId")
},
onNavigateToSearch = {
navController.navigate("search")
}
)
profileScreen(
onNavigateToSettings = {
navController.navigate("settings")
},
onNavigateToEditProfile = {
navController.navigate("edit-profile")
}
)
searchScreen(
onNavigateToResult = { resultId ->
navController.navigate("detail/$resultId")
}
)
}
}Key Takeaways
- 1Modularize when build times, merge conflicts, or code coupling become bottlenecks.
- 2Three module layers: :app (shell), :feature:* (screens), :core:* (shared infrastructure).
- 3Feature modules never depend on each other -- communicate through :core:model interfaces.
- 4Use internal visibility aggressively to enforce module boundaries.
- 5Convention plugins eliminate duplicate build configuration across modules.
- 6Expect 40-60% incremental build time reduction with proper modularization.
Frequently Asked
When should I modularize my app?
Consider modularization when: build times exceed 3-4 minutes, team grows beyond 5-6 developers, or you need dynamic feature delivery. Don't modularize prematurely - it adds complexity.
How many modules is too many?
There's no hard limit, but each module adds build configuration overhead. Start with 5-8 well-defined modules. Add modules when you have clear boundaries, not to hit a number.
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.