On this page
Two teams modularize the same 200-screen app. One splits by layer and ends up with a :data module that every change touches. The other splits by feature and gets independent teams and fast builds. The strategy you choose shapes your build times and your team's daily experience for years.
The main strategies
| Strategy | Pros | Cons | Fits |
|---|---|---|---|
| By layer | Simple; enforces layering | Every feature change touches every layer module; poor build parallelism; no team ownership | Small apps wanting layer enforcement |
| By feature | Independent features; ownership; good caching | Shared code needs a home; risk of duplication | Apps with clear product areas |
| Hybrid | Feature independence plus shared core | Requires discipline about what goes in core | Most medium and large apps |
| api/impl split | Consumers depend on a small API module; impl changes don't recompile consumers | Doubles module count for split features | Very large apps, cross-feature contracts |
The api/impl split
:feature:payments:api โ interfaces + models other features may use (tiny, stable)
:feature:payments:impl โ UI, ViewModels, repositories (changes often)
:feature:checkout โ depends on :feature:payments:api only
:app โ depends on :feature:payments:impl (to bind implementations via DI)interface PaymentLauncher {
fun paymentRoute(orderId: String): NavKey // checkout navigates without knowing the impl
}Because checkout sees only the small API module, editing payment screens doesn't recompile checkout. That pays off in large codebases.
Dependency rules worth enforcing
- Feature modules don't depend on other feature modules, only on their
:apimodules. - Core modules never depend on feature modules.
:core:modeland:core:domainare pure Kotlin (no Android dependencies).- Only
:appknows about every implementation.
Enforce these in CI with a dependency-analysis plugin or a custom Gradle check, not by convention alone.
Migrating a monolith step by step
- Measure: record clean and incremental build times and the main pain points.
- Set up build logic: a version catalog and convention plugins.
- Extract leaf core modules: model, design system, network and database. These have few dependencies on the rest.
- Extract one well-bounded feature, such as settings, to prove the pattern, including navigation and DI.
- Continue feature by feature, prioritising frequently changed or team-owned areas.
- Re-measure and adjust granularity.
Move code in small, behaviour-preserving PRs. Avoid long-lived modularization branches, which conflict with ongoing feature work.
Dynamic feature modules
Play Feature Delivery can deliver modules on demand or conditionally, which reduces initial download size for large, rarely used features (an AR viewer, an advanced editor). They add complexity: different navigation, testing and dependency direction. Reserve them for genuinely large, optional features.
Measuring success
- Incremental build time for typical changes.
- CI time and build-cache hit rate.
- Number of modules touched per PR (it should drop).
- Developer feedback on sync time and navigation.
Examples
E-commerce app
Hybrid: catalogue, product, cart, checkout and account features over core data, design system and analytics, with an api/impl split for payments.
Banking app
Strict feature isolation, security-sensitive core modules owned by a platform team, and dependency rules in CI.
Media app
Player as a core module, with a dynamic feature for offline downloads in markets where it's used.
Frequently asked questions
How big should a feature module be?
Roughly one product area owned by one team: a few screens with their ViewModels and data. Split further only for build or ownership reasons.
Where does shared UI go?
Generic components go in :core:designsystem. Feature-specific but shared UI may need a small shared module, but avoid a dumping-ground "common" module.
Does modularization help app size?
Not by itself. R8 already removes unused code. Dynamic features can reduce initial download size.
Conclusion
For most large Android apps, the winning strategy is hybrid: vertical feature modules over a disciplined set of core modules, with api/impl splits where cross-feature contracts are hot. Migrate incrementally, enforce dependency rules automatically, and measure the outcome.
Next step: apply design principles inside those modules with SOLID Principles in Android Development.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Architecture makes the most sense when you apply it to a real project and see why each boundary exists. Learn how maintainable Android applications are designed by building one.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.