Why Modularize?
The Monolith Problem
A single-module Android app has every feature in one app module. As the codebase grows, this creates problems:
- Build speed: Every change recompiles the entire module. A 500-file module takes minutes to build.
- Code boundaries: Nothing prevents the login feature from importing classes from the checkout feature.
- Team velocity: Merge conflicts increase as teams step on each other's code.
- Testing: You can't test the profile feature without building the entire app.
What Modularization Does
Modularization splits the app into separate Gradle modules, each with clear boundaries. A feature in the :feature:profile module cannot accidentally import a class from :feature:checkout unless you explicitly declare that dependency.
Module Types
- App Module (
app): The entry point. Contains the Application class, MainActivity, and dependency wiring. Depends on all feature modules but contains no business logic. - Feature Modules (
:feature:login,:feature:home,:feature:profile): Each module contains one user-facing feature — its screens, ViewModels, and navigation. Depends on core and domain modules. - Library Modules (
:core:network,:core:database,:core:domain): Shared infrastructure used by multiple features. Contains API clients, database, domain models, and utility classes.
Gradle Setup
// app/build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
dependencies {
implementation(project(":feature:home"))
implementation(project(":feature:profile"))
implementation(project(":feature:login"))
implementation(project(":core:network"))
implementation(project(":core:database"))
}
// feature/home/build.gradle.kts
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
}
dependencies {
implementation(project(":core:domain"))
implementation(project(":core:network"))
implementation(project(":core:designsystem"))
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
}
Dependency Direction and Boundaries
The Dependency Hierarchy
Modules form a directed acyclic graph (DAG). Dependencies must flow downward — features depend on core, core never depends on features.
app
┌──────┼──────┐
feature:home feature:login feature:profile
└──────┼──────┘
core:domain core:network core:database
Rules
- Feature modules never depend on each other. If
:feature:profileneeds data from:feature:cart, the data flows through:core:domain— not by depending on:feature:cartdirectly. - Core modules never depend on feature modules. Core is reusable infrastructure.
- The app module is the only module that depends on everything. It wires feature modules together via navigation.
Cross-Feature Communication
When features need to talk to each other, use one of these patterns:
Shared Domain Layer:
// :core:domain defines shared models and interfaces
class CartRepository {
suspend fun getCartItemCount(): Int
}
// :feature:profile reads cart count from core:domain
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val cartRepository: CartRepository
) : ViewModel()
Navigation with Deep Links:
// :feature:home navigates to :feature:profile via deep link
navController.navigate("profile/userId=123")
Event Bus (last resort):
Use SharedFlow or a lightweight event bus only when other patterns don't fit. Avoid because it makes data flow implicit and hard to trace.
Avoiding Circular Dependencies
// WRONG: feature:a depends on feature:b, feature:b depends on feature:a
// This fails Gradle sync
// RIGHT: Extract shared logic into core:shared
:feature:a → :core:shared ← :feature:b
Build Speed and Practical Considerations
How Modularization Speeds Up Builds
Gradle only rebuilds modules whose source code or dependencies changed. If you edit :feature:profile, only that module recompiles — :feature:home and :feature:login use cached outputs. On a large project this cuts build times from minutes to seconds for incremental changes.
Dynamic Feature Modules
Google Play supports dynamic delivery — downloading feature modules on demand. A 200MB app can ship as a 50MB base with the camera feature downloaded only when the user opens it.
// feature/camera/build.gradle.kts
plugins {
id("com.android.dynamic-feature")
}
// Depends on the app module (inverted for dynamic features)
dependencies {
implementation(project(":app"))
}
When NOT to Modularize
- Prototypes and MVPs: Modularization adds setup overhead. Get the product right first.
- Small apps under 10k lines: The overhead of maintaining 10 modules outweighs the benefits.
- Solo developers on short timelines: The coordination cost is low when one person owns everything.
Common Mistakes
- Too many tiny modules: Each module has Gradle overhead. 3-5 feature modules is usually right for a mid-size app.
- Circular dependencies: Always extract shared code into a core module.
- Leaking abstractions: Feature modules should not expose internal classes. Use
internalvisibility. - Ignoring ProGuard/R8: Each module may need its own consumer ProGuard rules.
Modularization Checklist
- Identify natural feature boundaries
- Create core modules for shared infrastructure
- Ensure dependency direction is always downward
- Use
internalfor module-private classes - Set up per-module CI for faster feedback
- Configure consumer ProGuard rules per module
- Measure build times before and after
Quiz
1. Which module type contains the MainActivity and Application class?
2. Why must feature modules never depend on each other?
3. How does modularization improve build speed?
4. What is the correct dependency direction in a modularized app?
Flashcards
Question
What are the three types of modules in a modularized Android app?
Click to reveal answer
Answer
App module (entry point, no business logic), Feature modules (one user-facing feature each), Library modules (shared infrastructure like network, database, domain).
Question
How do feature modules communicate with each other?
Click to reveal answer
Answer
Through shared core modules (domain layer), navigation with deep links, or an event bus as a last resort. Never by depending directly on each other.
Question
When should you NOT modularize an Android app?
Click to reveal answer
Answer
Prototypes/MVPs, small apps under 10k lines, or solo developers on short timelines where the coordination overhead outweighs the benefits.
Question
What is a dynamic feature module?
Click to reveal answer
Answer
A feature module that can be downloaded on demand via Google Play. The base app ships small, and features are fetched when the user opens them, reducing initial install size.
Revision Notes
Key Takeaways
- 1. Modularization splits the app into feature, library, and app modules with clear boundaries
- 2. Dependencies flow downward: app → features → core. Core never depends on features
- 3. Gradle only recompiles changed modules, making incremental builds significantly faster
- 4. 3-5 feature modules is the sweet spot — too many adds overhead, too few defeats the purpose
Interview Tips
- • Draw the module hierarchy and explain why dependencies flow downward
- • Discuss how feature modules communicate through core modules, not directly
- • Explain when modularization is overkill (prototypes, small apps)
- • Know the difference between regular library modules and dynamic feature modules
Cheat Sheet
Modularization Cheat Sheet
Module Types:
- App: Entry point, wires features, no business logic
- Feature: One user-facing feature (screens, ViewModels, navigation)
- Library/Core: Shared infrastructure (network, database, domain)
Dependency Hierarchy:
app
┌────┼────┐
feature feature feature
└────┼────┘
core:domain core:network core:database
Rules:
- Features never depend on other features
- Core never depends on features
- Dependencies always flow downward
- Use
internalfor module-private classes
Build Speed:
- Gradle caches module outputs
- Only changed modules recompile
- 3-5 feature modules is the sweet spot for mid-size apps
Cross-Feature Communication:
- Shared domain layer (preferred)
- Navigation with deep links
- Event bus (last resort)
Checklist:
- Natural feature boundaries identified
- Core modules for shared infra
- No circular dependencies
- Consumer ProGuard rules per module