Android & Mobile

Enterprise Android Architecture: Plan the Boundaries Before the Modules

Plan enterprise Android architecture around change boundaries, ownership, reproducible Gradle configuration, state recovery, module contracts, and risk-based CI instead of treating modularization as a folder exercise.

Table of Contents10 sections
An Android phone, laptop, and blank planning cards on an engineering desk.
A practical planning desk for making Android architecture boundaries explicit.

Architecture planning is a shared decision about ownership, boundaries, and verification.

A large Android app rarely becomes difficult because it has too few modules. It becomes difficult when nobody can answer basic change questions quickly: Who owns this capability? What is allowed to depend on it? What must be retested if it changes? Which state survives process death? Which build variants are affected?

That is the real enterprise Android architecture problem.

Android projects can contain multiple modules that are built and tested independently, and Android’s build system can use module boundaries to separate responsibilities and isolate rebuilds. But creating :feature:* and :core:* directories does not automatically create a healthy architecture. A poorly chosen boundary simply turns runtime coupling into Gradle coupling.

The planning goal should therefore be predictable change: a team should know the blast radius of a change before CI discovers it for them.

This guide uses that principle to plan a growing Android codebase around ownership, dependency direction, build configuration, state recovery, and verification.

Start With a Change Map, Not a Module Map

Before drawing the Gradle graph, list the kinds of change the product actually experiences.

Change Typical owner Likely blast radius Boundary signal
Checkout UI redesign Checkout team Checkout presentation Strong feature boundary
Authentication token policy Platform/security Many features Shared contract, narrow implementation
Database migration Data/platform Persisted features Central data boundary + migration tests
Design-system token change Design system Most screens Shared UI dependency
New enterprise customer Analyzing Technical Review Feedback Multi Flavor Release/platform Build + selected features Build-variant boundary
Analytics event rename Product analytics Multiple features Stable analytics contract

This exposes an important distinction: a module is an implementation mechanism; a change boundary is an architectural decision.

If two areas always change together, splitting them into separate modules may add ceremony without independence. If two teams routinely collide in one module but release and test their work independently, that is a much stronger reason to create a boundary.

A useful planning question is:

If this capability changed tomorrow, what is the smallest part of the repository that should need to understand the change?

The answer is often a better starting point than a generic architecture diagram.

Define the Dependency Rules Before Creating Modules

A scalable module graph needs rules that can be explained without opening Android Studio.

One workable shape is:

:app
  -> :feature:payments
  -> :feature:profile
  -> :feature:history

:feature:*
  -> :core:designsystem
  -> :core:model
  -> feature-owned contracts

:data / implementations
  -> network, database, SDK adapters

The exact names are less important than the direction.

For every proposed module, document four things:

  1. Responsibility , what business or platform capability does it own?
  2. Public contract , what is another module actually allowed to call?
  3. Forbidden dependencies , what must never leak across the boundary?
  4. Verification , which tests prove the contract still works?

Android library modules are useful when code or resources need to be reused, and Android projects can build, test, and debug modules independently. That makes modules valuable boundaries, but not free ones: each new module adds Gradle configuration, dependency decisions, visibility rules, and navigation or DI integration.

For a smaller codebase, packages inside one module can provide sufficient separation. Create a Gradle module when you need a stronger enforcement mechanism: independent ownership, reuse, build isolation, variant separation, or a contract that should be impossible to bypass accidentally.

For a concrete feature-level example, see Modularizing a Detail Screen in Android, which explores the cost as well as the benefit of splitting a screen into dedicated modules.

Treat Gradle Configuration as Product Infrastructure

Enterprise Android teams often centralize architecture discussions around Diagnosing Kapt Execution Failures In Kotlin classes while allowing build logic to evolve organically. That is backwards. If the repository cannot produce the same result on a clean machine and in CI, the architecture is already unreliable.

Establish a build contract that includes:

Centralize rules that must remain consistent, but do not turn the root build into a dumping ground. Shared convention plugins are useful when many modules need the same configuration because they make the policy executable instead of copying it across dozens of build.gradle.kts files.

Version catalogs solve a different problem: they provide a common vocabulary for dependency coordinates and versions. They should not become a substitute for deciding which modules are actually allowed to depend on a library.

A clean-build test remains one of the simplest architecture checks:

./gradlew clean assembleDebug testDebugUnitTest lintDebug

The exact tasks will differ by repository, but the principle is stable: the project definition should be sufficient without undocumented IDE state or a developer’s warm local environment.

Plan Variants as Architecture, Not Release Decoration

Enterprise apps frequently support staging environments, white-label customers, hardware integrations, regions, or free/pro distributions. Android build variants are composed from build types and product flavors, so variant design can quickly multiply the number of configurations the team must reason about.

Do not begin by creating every combination somebody might someday need. Define the dimensions that represent real product differences.

For each variant dimension, ask:

If five flavors and three build types create fifteen variants but the business ships only three meaningful combinations, blindly validating all fifteen may create cost without proportional confidence. Conversely, testing only debug can hide release-only dependency, shrinking, manifest, or configuration failures.

The architecture plan should explicitly name the supported variant matrix and the evidence required for each tier.

Give Every Important State an Owner and a Recovery Contract

Large apps fail in subtle ways when state ownership is implicit.

A screen may have several different kinds of state at once:

State Example Appropriate owner Recovery expectation
Ephemeral UI expanded section UI Can be recreated
Navigation/input current step, draft field UI/ViewModel + saved state Restore after recreation when valuable
Durable user data transfer draft repository/database Survive process death
Server truth account balance repository/API Refresh according to freshness policy
Derived presentation formatted total presentation logic Recompute

A generic UiState model is useful only after these ownership decisions are clear. Otherwise a ViewModel can become a convenient container for data that should have lived in persistence or in a feature contract.

For critical flows, write the recovery scenario before implementation:

Given: user is on step 3 of a transfer
When: Android kills the process in the background
Then: durable draft data is recovered
And: transient network requests are restarted safely
And: the user never sees a completed transfer that was not confirmed by the backend

That scenario is more useful to architecture planning than simply declaring that the project uses MVVM.

Design Quality Gates Around Risk

An enterprise CI pipeline should not be a ceremonial list of every test the repository owns. It should provide evidence proportional to the change.

A practical risk map might look like this:

Changed area Minimum evidence
Pure Kotlin domain rule unit tests
Compose/UI component unit/UI or screenshot evidence as appropriate
Room schema migration + DAO/repository tests
Gradle/build logic clean configuration + representative assemble
Manifest/permissions manifest/variant verification + targeted device test
Authentication/networking contract tests + failure-path verification
Shared core module affected feature tests + dependency graph review

Then keep a smaller set of release gates for concerns that cannot be inferred cheaply from changed files: release assembly, signing configuration, shrinking, critical end-to-end flows, and supported-device checks.

This creates two feedback loops:

Pull request: fast, change-aware evidence.

Release: broader system confidence.

The distinction matters. A pipeline that takes an hour for a documentation-adjacent Kotlin change teaches developers to avoid CI; a pipeline that skips release-specific checks teaches them to distrust it.

Make Ownership Visible in the Repository

Architecture degrades when a dependency graph exists but organizational ownership does not.

For each major feature or platform module, record:

Module: :feature:payments
Owner: Payments team
Public contract: PaymentEntry, PaymentResult
Depends on: :core:model, :core:designsystem
Must not depend on: :feature:history
Critical tests: payment-domain, payment-navigation
Release variants: staging, production

This small contract improves code review because reviewers can distinguish a legitimate dependency from an architectural shortcut.

It also makes refactoring measurable. If :feature:payments suddenly needs five unrelated :core modules and another feature’s implementation module, the graph is telling you that the boundary is becoming porous.

Do Not Modularize Everything at Once

A monolith-to-multimodule migration is safest when the architecture can prove value incrementally.

Use this sequence:

1. Baseline the pain

Measure build time, CI duration, ownership collisions, frequently changed packages, and flaky or expensive test areas. Without a baseline, a migration can feel architecturally elegant while making delivery slower.

2. Extract one high-signal boundary

Choose a capability with clear ownership and limited dependencies. Avoid starting with the most entangled feature merely because it hurts the most.

3. Define the contract first

Decide what consumers need before moving implementation. A module that exposes its entire internal model has not created much isolation.

4. Move verification with the boundary

The module should carry the tests that prove its public behavior. Otherwise the code moves but confidence remains coupled to the monolith.

5. Measure the result

Check whether the extraction improved parallel development, test focus, build isolation, or ownership clarity. If it did not, understand why before repeating the pattern twenty times.

A Planning Checklist for a Growing Android Codebase

Before approving a new architecture direction, verify that the team can answer these questions:

If several answers are unclear, adding more modules will probably make the uncertainty more expensive rather than solve it.

The RayLabs Rule: Optimize for Predictable Change

The useful output of enterprise Android architecture is not a perfect diagram. It is a repository where change has understandable consequences.

A healthy architecture lets a developer make a feature change and predict which module owns it, which dependencies are legal, which state can be lost, which variants matter, and which tests should fail if the change is wrong.

That is a stronger target than “use Clean Architecture,” “use MVVM,” or “make the app multi-module.” Those can all be reasonable implementation choices, but none of them replaces the planning work.

Start with one recurring source of uncertainty in your current Android project. Turn it into an explicit boundary, contract, owner, and verification rule. If that makes the next change easier to predict, the architecture is doing useful work.

Continue Exploring

You Might Also Like

View all articles