Modularization & Multi-Module Architecture

📖 Concept

Modularization splits a monolithic app into independent Gradle modules. At senior level, this is essential knowledge — all large Android apps at Google use multi-module architecture.

Why modularize:

  1. Build speed — Only changed modules recompile. Parallel module compilation.
  2. Team scalability — Teams own modules independently with clear boundaries.
  3. Code isolation — Prevents accidental coupling, enforces API boundaries via internal visibility.
  4. Dynamic delivery — Play Feature Delivery for on-demand modules.
  5. Reusability — Shared modules (design system, analytics) across apps.

Module types:

:app                    → Application module (thin, just wiring)
:feature:home           → Feature module (UI + ViewModel)
:feature:profile        → Feature module
:feature:settings       → Feature module
:core:data              → Data layer (repositories, data sources)
:core:domain            → Domain layer (use cases, models)
:core:network           → Network layer (Retrofit, interceptors)
:core:database          → Database layer (Room, DAOs)
:core:ui                → Shared UI components (design system)
:core:common            → Utilities, extensions

Dependency rules:

  • :feature:* → depends on :core:*
  • :core:data → depends on :core:domain, :core:network, :core:database
  • :core:domain → NO dependencies (pure Kotlin)
  • :feature:* → does NOT depend on other :feature:* modules
  • :app → depends on all :feature:* modules

Navigation between features: Since features can't depend on each other, cross-feature navigation uses: Navigation component with deep links, or a shared navigation module with route definitions.

💻 Code Example

codeTap to expand ⛶
1// settings.gradle.kts — Module structure
2include(
3 ":app",
4 ":feature:home",
5 ":feature:profile",
6 ":feature:settings",
7 ":core:data",
8 ":core:domain",
9 ":core:network",
10 ":core:database",
11 ":core:ui",
12 ":core:common"
13)
14
15// :feature:home/build.gradle.kts
16plugins {
17 id("com.android.library")
18 id("dagger.hilt.android.plugin")
19}
20
21dependencies {
22 implementation(project(":core:domain"))
23 implementation(project(":core:ui"))
24 implementation(project(":core:common"))
25 // CANNOT depend on :feature:profile — features are isolated
26}
27
28// Cross-feature navigation via shared routes
29// :core:common/src/.../Navigation.kt
30object Routes {
31 const val HOME = "home"
32 const val PROFILE = "profile/{userId}"
33 const val SETTINGS = "settings"
34
35 fun profileRoute(userId: String) = "profile/$userId"
36}
37
38// :app/src/.../NavGraph.kt
39@Composable
40fun AppNavGraph(navController: NavHostController) {
41 NavHost(navController, startDestination = Routes.HOME) {
42 // Each feature provides its own navigation subgraph
43 homeNavGraph(navController)
44 profileNavGraph(navController)
45 settingsNavGraph(navController)
46 }
47}
48
49// :feature:home — provides its nav graph as an extension function
50fun NavGraphBuilder.homeNavGraph(navController: NavController) {
51 composable(Routes.HOME) {
52 HomeScreen(
53 onProfileClick = { userId ->
54 navController.navigate(Routes.profileRoute(userId))
55 }
56 )
57 }
58}
59
60// Convention plugins for consistent module config
61// build-logic/convention/src/.../AndroidFeaturePlugin.kt
62class AndroidFeaturePlugin : Plugin<Project> {
63 override fun apply(target: Project) {
64 with(target) {
65 pluginManager.apply("com.android.library")
66 pluginManager.apply("dagger.hilt.android.plugin")
67 dependencies {
68 add("implementation", project(":core:ui"))
69 add("implementation", project(":core:domain"))
70 }
71 }
72 }
73}

🏋️ Practice Exercise

Practice:

  1. Refactor a monolithic app into at least 4 modules and measure build time improvement
  2. Create a convention plugin that standardizes feature module configuration
  3. Implement cross-feature navigation without direct module dependencies
  4. Draw the dependency graph for a 10-module app and ensure no circular dependencies
  5. Set up a feature module with its own Hilt component

⚠️ Common Mistakes

  • Creating too many modules too early — start with 3-4 modules, split as needed

  • Circular dependencies between feature modules — features must be isolated, communicate via shared contracts

  • Putting all code in :core:common — it becomes a god module; split by responsibility

  • Not using convention plugins — each module has different Gradle config, causing maintenance overhead

  • Forgetting to use internal for module-private APIs — everything defaults to public in Kotlin

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Modularization & Multi-Module Architecture. Login to unlock this feature.