Mobile App Modularization Strategy

Production-ready guide covering mobile app modularization strategy with implementation patterns, code examples, and anti-patterns for enterprise engineering teams.

Mobile apps grow from a single codebase into complex systems with multiple teams, features, and release cadences. A modularization strategy is the deliberate organization of app code into cohesive, independently developable units—modules—that can be built, tested, and deployed in isolation. It matters when a mobile app exceeds 100,000 lines of code, teams exceed five developers, or release cycles span multiple weeks. Without modularity, changes to one feature break another, developers wait for CI pipelines to complete before shipping, and technical debt accumulates in monolithic codebases.

Core Principles of Modularization

Define Module Boundaries by Domain

Each module must encapsulate a cohesive business capability. A *user profile* module, for example, includes all code related to displaying, editing, and persisting user data: UI components, data models, network services, and local storage logic. Modules should be independently testable, versionable, and deployable.

A module is defined by a root directory containing:

The module.json file in a Kotlin-based Android project defines the module’s identity:

{
  "name": "user-profile",
  "version": "1.0.0",
  "description": "UI and logic for user profile management",
  "dependencies": [
    "core-ui:1.2.0",
    "networking:0.8.1",
    "data-models:2.0.0"
  ],
  "exports": [
    "com.example.userprofile.ui",
    "com.example.userprofile.data"
  ],
  "build": {
    "sourceSets": {
      "main": {
        "java": "src/main/kotlin",
        "resources": "src/main/resources"
      },
      "test": {
        "java": "src/test/kotlin",
        "resources": "src/test/resources"
      }
    }
  }
}

Modules are resolved at build time using a dependency graph. The build system resolves dependencies by name and version, and exports public APIs via the exports list. A consuming module may import user-profile and use com.example.userprofile.ui.ProfileScreen without knowing its internal implementation.

Use Dependency Graphs for Build and Release Coordination

The build system generates a dependency graph from module declarations. For example, app depends on user-profile, orders, and settings. When user-profile changes, the build system can incrementally rebuild only modules that depend on it.

In Gradle, the --build-cache and --configure-on-demand flags significantly improve incremental builds:

./gradlew build --build-cache --configure-on-demand --continue

This command:

A common failure mode: a module’s build.gradle is missing apply plugin: 'com.android.library', causing the module to be built as an application instead of a library, which breaks dependency resolution. The error is silent: the module compiles, but its classes are not available in the consuming module.

Another failure: dependencies in module.json are specified with mismatched version ranges. A module declares "core-ui:1.2.0", but the build resolves core-ui:1.3.0 from a different source. The resulting ClassNotFoundException points to com.example.coreui.UIUtils, but the core-ui module is not present in the final APK or IPA.

Implement Incremental Builds with Build Artifacts

Each module produces a build artifact—a .jar, .aar, .ipa, .apk, or .framework—that contains its compiled code, resources, and metadata. These artifacts are published to a central module repository, typically a Maven or Artifactory server.

Publishing a module to a local Maven repository:

./gradlew publishToMavenLocal

This command generates:

The consuming app includes the module via:

dependencies {
    implementation 'com.example:profile:1.0.0'
    implementation 'com.example:networking:0.8.1'
}

The build system resolves the pom.xml file for each module and downloads the corresponding .jar and metadata.

A silent failure occurs when the pom.xml is not generated correctly. The groupId is missing, so com.example:profile:1.0.0 resolves to com.example.profile, not com.example.profile. The classifier field is ignored, so user-profile-1.0.0-sources.jar is not picked up during dependency resolution.

Isolate Module Versions in CI/CD Pipelines

Each module has its own CI pipeline that runs on every commit. A change to user-profile triggers:

The CI pipeline is triggered by a git push origin main to a module’s repository.

A common mistake: modules are not versioned independently. A user-profile module is updated in main, but the app module depends on user-profile:1.0.0, even though the latest version is 1.0.5. The app module is built and deployed with an outdated user-profile module.

To fix this, each module maintains its own version.txt file:

1.0.5

The CI pipeline reads this file to determine the version to publish. The publishToMavenLocal task uses the version from version.txt in the build.gradle:

version = file('version.txt').text.trim()

This ensures that every module’s version is explicit and atomic.

Enable Feature Flags and Dynamic Module Loading

Modules can be loaded at runtime based on feature flags. A FeatureFlag service reads a configuration file or API endpoint to determine which modules should be loaded.

A FeatureFlag configuration file:

{
  "features": {
    "user-profile": true,
    "orders": false,
    "notifications": true,
    "analytics": true
  },
  "version": "2.1.0"
}

At startup, the app checks FeatureFlag and dynamically loads the user-profile and notifications modules. The orders module is downloaded and cached for later use.

The ModuleLoader class in Kotlin:

class ModuleLoader {
    suspend fun loadModules(featureFlags: Map<String, Boolean>): List<Module> {
        val loaded = mutableListOf<Module>()
        for ((name, enabled) in featureFlags) {
            if (enabled) {
                val module = downloadModule(name)
                module.load()
                loaded.add(module)
            }
        }
        return loaded
    }
}

A failure mode: the downloadModule function fails silently if the module’s pom.xml is missing. The app downloads the .jar, but the Module class is not found in the META-INF/MANIFEST.MF, causing a ClassNotFoundException at runtime.

Manage Shared Code and Platform Differences

Shared code—common models, utilities, and constants—resides in a shared module. This module is consumed by both Android and iOS apps.

The shared module exports:

On Android, this module is included as a .jar in the app module. On iOS, it is compiled into a .framework using a build script:

cd shared && \
    ./gradlew build -Pplatform=ios && \
    cp build/ios/framework/Shared.framework ../ios/Shared.framework

A subtle issue: the shared module uses Kotlin’s @Parcelize annotation, but the app module fails to find Parcelable implementations because the androidx.core library is not declared in shared/build.gradle.

The solution: declare androidx.core:core:1.10.1 in shared/build.gradle, and use apply plugin: 'kotlin-android' to enable Android-specific Kotlin compilation.

Failure Modes and Sharp Edges

Summary of Key Commands and Configurations

TaskCommand or FileExample
Define modulemodule.json{"name": "user-profile", "version": "1.0.0"}
Publish module./gradlew publishToMavenLocalGenerates .jar, .pom, and version.txt
Consume moduleimplementation 'com.example:profile:1.0.0'In build.gradle
Version moduleversion.txt1.0.5
Load module at runtimeFeatureFlag JSON{"features": {"user-profile": true}}
Build for iOS./gradlew build -Pplatform=iosGenerates .framework
Check module exportsjar -tf user-profile-1.0.0.jarLists module-info.class, META-INF/, and exported packages

A modularized mobile app is not a set of folders. It is a system of interdependent, independently versioned units, each with a clear responsibility, a defined interface, and a consistent build and deployment process. When a module fails, the error is localized. When a new feature is added, it is pluggable. The app evolves not as a monolith, but as a constellation of modules.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.