Modular Architecture Implementation for Mobile Apps
Monolith with 50+ screens in a single module—story of 8 developers waiting 4 minutes per build, one team's feature breaking another's tests because both import the same singleton. Modular architecture solves these exact problems. Not "future scalability"—but slow builds and merge conflicts right now.
How We Divide into Modules
Standard approach—feature-based division with common layer:
:app — entry point, DI graph, navigation
:core:network — HTTP client, interceptors
:core:storage — database, SharedPreferences/DataStore
:core:ui — common components, theme
:core:auth — tokens, AuthInterceptor
:feature:profile — user profile
:feature:catalog — product catalog
:feature:checkout — order checkout
:feature:orders — order history
Each :feature:* depends only on :core:* and knows nothing of other features. Feature navigation—via abstraction: NavigationManager in :core, used by each feature without direct import of another. Android: Navigation Component with NavGraph at :app level, deep links registered in module manifests. iOS: Coordinator pattern or Router with protocol-based navigation.
Android. Each :feature:* is separate Gradle module (:feature:catalog → catalog/build.gradle.kts). Gradle Configuration Cache + Build Cache (~/.gradle/caches) radically reduce rebuild time: change only :feature:checkout—only it rebuilds. --configuration-cache in Gradle 8.x stable. Kapt → KSP: migrate annotation processors from Kapt to KSP (Kotlin Symbol Processing) for +30–50% code generation speed.
iOS. Modules via Swift Packages (local) or Framework targets in Xcode workspace. SPM supports local packages via path: in Package.swift. Workspace with multiple projects (Feature1.xcodeproj, Feature2.xcodeproj)—deprecated; new: monorepo with local SPM packages + single main .xcodeproj. Tuist or XcodeGen auto-generate .xcodeproj from config files, eliminating .pbxproj merge conflicts.
DI in Modular Architecture
DI graph shouldn't be monolithic. Android: Hilt with @InstallIn(SingletonComponent::class) for core dependencies and @InstallIn(ViewModelComponent::class) for feature-specific. Each feature declares its @Module classes; Hilt compiles graph at compile time. This means: if :feature:catalog not linked in :app—its DI module doesn't compile, doesn't affect build.
iOS: manual DI via Resolver/Swinject, or clean factory pattern without DI framework. DI framework on Swift less critical in modular apps: dependencies passed via initializers, Composition Root in :app assembles everything.
Dynamic Delivery and Optional Modules
Android allows feature modules on-demand via Play Feature Delivery. Users download base app, heavy features (AR try-on, offline maps)—on first use. SplitInstallManager manages loading, SplitCompatActivity activates. iOS has no equivalent—App Clips fill different niche.
Case study. Retail app: 6 teams, 80+ screens, Android + iOS. Before modularity: full Android build—7 minutes, iOS—11 minutes. After 14-module split with Build Cache: incremental rebuild changing one feature—45–90 seconds. Merge conflicts in shared code dropped ~threefold. Parallel feature development without inter-team blocking.
Timeline
Modularizing existing app—labor-intensive refactoring:
| Project Size | Estimated Timeline |
|---|---|
| 20–40 screens, 1–2 teams | 4–8 weeks |
| 50–100 screens, 3–5 teams | 2–4 months |
| 100+ screens, complex dependencies | 4–8 months |
Pricing determined individually after architecture audit and dependency analysis of existing code.
Common Self-Modularization Mistakes
Circular dependencies. :feature:profile imports :feature:orders, :feature:orders imports :feature:profile. Gradle won't build—circular dependency. Fix: extract shared models to :core:domain or :shared:models, both features depend only on it.
Too fine-grained modules. :core:extensions with 5 extension functions—overhead without benefit. Rational minimum: module justified if modifiable independently and doesn't depend on modules changing more often.
Resource ID conflicts. Android combining resources from multiple modules causes name collisions. Convention: module prefix for all resources (catalog_item_card_background, not just item_card_background). R class isolation via android.nonTransitiveRClass=true in gradle.properties—mandatory for large projects.







