android

Android Modularization: Why Single-Module Projects Hit a Wall

If your Android codebase lives in one fat `app` module, you already know the pain: a one-line change triggers a full rebuild, merge conflicts multiply, and onboarding new devs stretches into weeks. Modularization escapes this trap by splitting the project into self-contained pieces, each owning a slice of functionality, so the build only recompiles what changed.

Key facts

  • Modularization splits the app by feature and by layer, cutting build times and clarifying ownership.
  • The presentation layer uses feature modules (`feature_home`, `feature_profile`, `feature_settings`), each with its own Compose UI, ViewModel and dependency injection wiring.
  • A domain layer holds use cases, models and repository interfaces, kept free of Android dependencies for easy testing.
  • A data layer implements those interfaces with Ktor for networking, Room for storage and DataStore for preferences.
  • Common code lives in a `core` module with utilities, base classes and shared dependency injection setup.

Why single-module projects stop scaling

A single `app` module is fine for a prototype. It is miserable for a product. Every commit recompiles everything, pull requests collide, and test cycles stretch. Teams cannot ship features in parallel without stepping on each other. Adding a new screen means touching the same codebase, which is where bugs breed.

How clean architecture splits the work

The fix follows clean architecture and SOLID principles. The presentation layer is sliced into feature modules. Each contains its own Compose UI, ViewModel and dependency injection wiring. That gives every feature a clear owner and a fast incremental build.

The domain layer sits below: entities, use cases and repository interfaces, with no Android imports. Keeping this layer pure means unit testing business logic without spinning up an emulator. The data layer then implements the domain interfaces using Ktor for APIs, Room for the local database and DataStore for key-value preferences. Shared code, base ViewModels and constants live in a `core` module so features do not reinvent the wheel.

Single Responsibility maps directly onto this split. `UserRepository` deals only with data; ViewModels handle UI state; use cases orchestrate the two. Each module has one reason to change.

Our analysis

This is less a technical choice and more a workflow choice. Modular Android projects line up with how agile teams actually work. A squad owning `feature_profile` can merge, test and ship without waiting for `feature_home` to clear CI. Sprint planning gets cleaner: features become packages, ownership becomes obvious, and code review boundaries match team boundaries.

The trade-off is real. You pay upfront in build configuration, dependency injection setup, and a learning curve for developers used to one giant module. Smaller projects do not need it. But once a product lives longer than a year or grows past a single squad, the cost of not modularizing is paid forever in slow builds and brittle code.

Who should care

Team / Role Wins Risks
Large teams (4+ engineers) Parallel feature work without merge collisions; clearer ownership Higher initial configuration overhead
Enterprise / long-lived apps Maintainability at scale; testability of business logic Requires discipline to keep layers clean
Product managers Faster sprint cycles; isolated feature rollouts Migration planning eats into roadmap time
Solo devs / small MVPs Still useful for testability and code organization Overhead may outweigh benefits until app grows

Incremental adoption works well: many teams start by extracting one feature module and one `core` module, then pull features out as the pain shows up.

Worked example: try this in your next sprint

Say you are a tech lead with a monolithic app and three engineers fighting merge conflicts on the settings screen. Pick one low-risk feature, say `feature_settings`, and extract it in a single sprint:

  • Move settings screens, ViewModels and navigation into a new `feature_settings` module.
  • Add a `domain` interface for reading and writing user preferences.
  • Implement that interface in the data layer using DataStore.
  • Wire dependency injection so `feature_settings` depends on `domain` and `core`, never on `app`.
  • Measure: build time for a settings-only change, and PR turnaround for that team.

If the numbers move, the case is made. Keep pulling features out one sprint at a time until the monolith is a thin shell.

What to watch next

Bottom line: modularization is no longer optional for serious Android codebases. The pattern is mature, the tooling around Gradle, Hilt and Compose supports it, and the productivity gains show up in the first sprint after extraction. Start small, pick a feature that hurts, and let the build time numbers make the argument for you.

Key takeaways

  • Modularization divides an Android project into feature modules and clean architecture layers, so only the changed piece recompiles.
  • Feature modules (`feature_*`) own UI, ViewModels and dependency injection for a single screen or flow.
  • The domain layer stays free of Android dependencies, making business logic easy to unit test.
  • The data layer implements domain interfaces with Ktor, Room and DataStore.
  • A `core` module holds shared utilities, base classes and constants.
  • Teams with parallel workstreams benefit most; the upfront cost pays off once a codebase passes a certain size.

Frequently asked questions

What is Android modularization?

It is the practice of splitting a single Android `app` module into multiple smaller modules by feature and by clean architecture layer, so each piece builds and tests independently.

How does modularization speed up Android builds?

Gradle only needs to recompile the module that changed, plus its dependents, instead of rebuilding the entire application on every commit.

What goes in the domain layer of a modular Android project?

Business logic, use cases, domain models and repository interfaces, kept free of Android dependencies so they can be unit tested without a device.

What is the role of the core module?

It holds shared utilities, base classes, constants and common dependency injection setup that every feature module can reuse.

When does a project actually need modularization?

Once the codebase slows down builds, blocks parallel feature work, or makes onboarding painful, modularization becomes worth the upfront migration cost.

Related