Android Modularization in 2026: Multi-Module Architecture and Interview Questions
Master Android multi-module architecture with convention plugins, Gradle version catalogs, and feature modules. Includes common interview questions on modularization strategies.

Android modularization transforms monolithic codebases into maintainable, scalable systems where teams work in parallel without stepping on each other's code. A well-modularized Android app builds faster, tests in isolation, and onboards new developers in days rather than weeks.
Modularization splits an Android app into independent Gradle modules with clear boundaries. Low coupling between modules and high cohesion within each module are the guiding principles. Google's Now in Android reference app demonstrates this pattern with 40+ modules.
Why Multi-Module Architecture Matters for Android Apps
A single-module Android app recompiles everything when one line changes. The networking layer modification triggers UI recompilation. Two developers editing different features create merge conflicts in shared files. Build times stretch to 4+ minutes on incremental builds.
Multi-module architecture addresses these pain points directly:
- Faster builds: Gradle only recompiles affected modules. A change in
:feature:checkoutleaves:feature:profileuntouched. - Parallel development: Teams own separate modules with defined APIs. Merge conflicts drop significantly.
- Isolated testing: Unit tests run against a single module without loading the entire app.
- Code reuse: A
:core:uidesign system module becomes a library usable across multiple apps.
The official Android modularization guide recommends structuring modules around features or layers that change together, following the Single Responsibility Principle.
Module Types and Their Responsibilities
The Now in Android repository establishes a proven module taxonomy that scales to enterprise apps.
include(":app")
// Core modules - shared utilities
include(":core:common")
include(":core:data")
include(":core:database")
include(":core:datastore")
include(":core:designsystem")
include(":core:domain")
include(":core:model")
include(":core:network")
include(":core:ui")
// Feature modules - screen-specific logic
include(":feature:home")
include(":feature:search")
include(":feature:bookmarks")
include(":feature:settings")App module: Contains MainActivity, navigation setup, and app-level scaffolding. Depends on all feature modules and required core modules.
Core modules: Provide shared functionality across features. :core:network handles API calls. :core:database manages Room entities. :core:designsystem defines the app's visual components.
Feature modules: Encapsulate a single screen or user flow. Each feature module depends only on the core modules it needs, never on other feature modules.
This structure enforces unidirectional dependencies: features depend on core, and app depends on features.
Gradle Version Catalogs for Dependency Management
Managing dependencies across 25+ modules without version conflicts requires centralized configuration. Gradle Version Catalogs solve this with a single libs.versions.toml file.
# gradle/libs.versions.toml
[versions]
agp = "8.7.0"
kotlin = "2.1.0"
compose-bom = "2026.09.00"
ksp = "2.1.0-1.0.29"
hilt = "2.54"
room = "2.7.0"
retrofit = "2.11.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.15.0" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
[bundles]
compose = ["androidx-compose-ui", "androidx-compose-material3"]
room = ["room-runtime", "room-ktx"]
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }Modules then reference dependencies with type-safe accessors:
dependencies {
implementation(libs.bundles.compose)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}Gradle generates these accessors during sync, enabling IDE autocompletion and compile-time validation.
Ready to ace your Android interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Convention Plugins: DRY Build Configuration
A 25-module project without convention plugins repeats the same compileSdk, minSdk, composeOptions, and plugin applications in each build.gradle.kts. Different modules can accidentally use different configurations.
Convention plugins centralize this logic in a build-logic included build.
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.android.library")
pluginManager.apply("org.jetbrains.kotlin.android")
extensions.configure<LibraryExtension> {
compileSdk = 35
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
}
}
}Register the plugin in build-logic/convention/build.gradle.kts:
plugins {
`kotlin-dsl`
}
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
}
gradlePlugin {
plugins {
register("androidLibrary") {
id = "myapp.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
register("androidFeature") {
id = "myapp.android.feature"
implementationClass = "AndroidFeatureConventionPlugin"
}
register("androidHilt") {
id = "myapp.android.hilt"
implementationClass = "AndroidHiltConventionPlugin"
}
}
}Feature modules now require minimal configuration:
plugins {
alias(libs.plugins.myapp.android.feature)
alias(libs.plugins.myapp.android.hilt)
}
dependencies {
implementation(projects.core.data)
implementation(projects.core.designsystem)
}Google's Now in Android build-logic uses this exact pattern.
Module Communication Patterns
Feature modules must not depend on each other directly. This constraint preserves parallel compilation and prevents circular dependencies. Three patterns handle cross-feature communication.
Navigation via Shared Routes
object Routes {
const val HOME = "home"
const val PRODUCT_DETAIL = "product/{productId}"
const val CHECKOUT = "checkout"
fun productDetail(productId: String) = "product/$productId"
}Feature modules import routes from :core:navigation without knowing which module implements each destination.
Shared Domain Models
data class Product(
val id: String,
val name: String,
val price: BigDecimal,
val imageUrl: String
)Both :feature:catalog and :feature:checkout depend on :core:model for the Product class.
Event Bus or Shared ViewModel
For runtime communication, a shared event bus in :core:common or a scoped ViewModel handles cross-feature events:
object CartEventBus {
private val _events = MutableSharedFlow<CartEvent>()
val events: SharedFlow<CartEvent> = _events.asSharedFlow()
suspend fun emit(event: CartEvent) {
_events.emit(event)
}
}
sealed interface CartEvent {
data class ItemAdded(val productId: String) : CartEvent
data class ItemRemoved(val productId: String) : CartEvent
}Interview Questions on Android Modularization
Modularization appears frequently in senior Android interviews. Interviewers assess understanding of build systems, architecture decisions, and practical trade-offs.
When discussing modularization in interviews, reference concrete metrics: build time improvements, team velocity gains, or specific module counts from projects. Abstract answers about "better separation of concerns" are less compelling than "build times dropped from 4 minutes to 45 seconds after modularization."
Q: What's the difference between api and implementation dependencies in Gradle?
implementation dependencies are internal to the module. Consumers of the module cannot access them. api dependencies are exposed transitively: if module A uses api(libs.retrofit), module B depending on A can use Retrofit classes directly.
Rule of thumb: use implementation by default. Only use api when the dependency is part of the module's public API. Overusing api breaks encapsulation and slows builds.
Q: How do you handle navigation between feature modules that don't depend on each other?
Create a :core:navigation module containing route definitions and navigation interfaces. Feature modules depend on this shared module. The app module wires destinations to implementations. This follows the dependency inversion principle: features depend on abstractions, not concrete implementations.
Q: What are the trade-offs of having too many modules?
Each module adds Gradle configuration overhead: build files, resource processing, manifest merging. Initial sync times increase. The tipping point varies by project, but 50+ modules is common in large apps. The benefits (parallel builds, isolated tests, clear ownership) typically outweigh the overhead for teams of 5+ developers.
Q: How do convention plugins differ from buildSrc?
buildSrc changes trigger a full project rebuild. Convention plugins in an included build only recompile when the plugin code changes, not when consuming modules change. This makes convention plugins faster for iterative development. Additionally, convention plugins can be published as a separate artifact for reuse across repositories.
Q: How do you test a feature module in isolation?
Feature modules depend on core modules through interfaces. Create fake implementations in test fixtures:
class FakeProductRepository : ProductRepository {
private val products = mutableListOf<Product>()
override suspend fun getProducts(): List<Product> = products
fun addProduct(product: Product) {
products.add(product)
}
}Feature module tests inject fakes via Hilt's testing APIs or manual constructor injection.
For more practice questions on Android architecture, see the MVVM architecture interview questions and dependency injection module.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Key Takeaways for Multi-Module Android Projects
- Structure modules by feature (
:feature:home,:feature:checkout) and layer (:core:data,:core:ui). Features depend on core, never on each other. - Use Gradle Version Catalogs (
libs.versions.toml) to centralize dependency versions. Bundles group related dependencies for cleaner build files. - Implement convention plugins in
build-logicto eliminate duplicated configuration across modules. One plugin change updates all consuming modules. - Enforce unidirectional dependencies: app depends on features, features depend on core. Tools like Dependency Guard can automate enforcement.
- Measure build performance before and after modularization. Track incremental build times, not just clean builds, to validate improvements.
- Keep module boundaries stable. Frequent restructuring negates the benefits of modularization.
Can you spot the bug in Android?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 17, 2026
Tags
Share
Related articles

Jetpack Navigation Compose in 2026: Type-Safe Navigation and Interview Questions
Master Jetpack Navigation Compose with type-safe routes using Kotlin Serialization. Learn Navigation 2.10, predictive back, argument passing, and common interview questions.

Android WorkManager in 2026: Background Tasks, Constraints and Interview Questions
Master Android WorkManager for reliable background task execution. Learn constraints, chaining, periodic work, and common interview questions with practical Kotlin examples.

Kotlin Flow vs StateFlow vs SharedFlow: Android Interview Questions in 2026
The Kotlin Flow vs StateFlow vs SharedFlow questions Android interviewers ask in 2026, with clear answers, a comparison table, and production-ready code.