๐Ÿ›๏ธ Android Architecture and Clean Code

Multi-Module Architecture in Android: Pros, Cons, and Lessons

Splitting an Android app into Gradle modules can speed up builds, enforce architecture and let teams work independently. It can also create slow syncs, circular-dependency headaches and boilerplate. Here are the real pros, the real costs, and the lessons that decide whether modularization pays off.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Advanced
On this page
  1. What modules give you
  2. A typical structure
  3. api vs implementation
  4. Convention plugins: avoid copy-pasted build files
  5. Navigating between features without coupling
  6. Lessons learned
  7. When it pays off
  8. Frequently asked questions
  9. Conclusion

A single :app module is the right start for almost every project. As the app grows to dozens of screens and several developers, one module means every change recompiles everything, any class can depend on any other, and ownership is blurry. Modularization addresses those problems, at a price.

What modules give you

Benefits

  • Build speed: unchanged modules come from the cache, and builds can parallelise
  • Enforced boundaries: the compiler blocks forbidden dependencies
  • Encapsulation: internal hides implementation details
  • Ownership: teams own modules
  • Reuse: shared design system and core modules
  • Dynamic delivery: optional on-demand feature modules

Costs

  • More Gradle configuration to maintain
  • Slower Gradle sync with many modules
  • Cross-feature navigation needs indirection
  • Visibility decisions (api vs implementation, internal)
  • Over-modularization makes simple changes touch many modules

A typical structure

Feature and core module dependency graph The app module depends on feature modules: home, search and settings. Feature modules depend on core modules: designsystem, data, domain and ui. Core data depends on core network and core database. Feature modules never depend on each other. :app :feature:home :feature:search :feature:settings :core:designsystem :core:domain :core:data :core:ui / :core:model :core:network :core:database
Dependencies flow downward. Features never depend on other features, which keeps each one independently buildable.

api vs implementation

feature/search/build.gradle.ktsGradle KTS
plugins { id("app.android.feature") }        // convention plugin (see below)

dependencies {
    implementation(project(":core:data"))       // used internally: NOT exposed to consumers
    implementation(project(":core:designsystem"))
    api(project(":core:model"))                 // types appear in this module's public API
}

Prefer implementation. It keeps dependencies from leaking to consumers and limits recompilation when they change. Use api only when a dependency's types appear in your module's public signatures.

Convention plugins: avoid copy-pasted build files

build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.ktKotlin
class AndroidFeatureConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) = with(target) {
        pluginManager.apply("com.android.library")
        pluginManager.apply("org.jetbrains.kotlin.android")
        pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
        extensions.configure<LibraryExtension> {
            compileSdk = 36
            defaultConfig.minSdk = 24
            buildFeatures.compose = true
        }
        dependencies {
            add("implementation", project(":core:designsystem"))
            add("implementation", project(":core:ui"))
        }
    }
}

Each new feature module then needs only a few lines. Google's Now in Android sample uses this approach.

If :feature:home needs to open search, it shouldn't depend on :feature:search. Common approaches:

  • Route keys in a shared module (:core:navigation), with each feature providing its entries, and :app assembling the graph.
  • Lambdas passed from :app: HomeScreen(onOpenSearch = { backStack.add(Search) }).

Lessons learned

Modularize for a reason

Measure build times or identify ownership pain first. "Clean architecture says so" isn't enough.

Start with core extraction

Moving the design system, network and database into modules usually brings the biggest early gains.

Watch the graph

Visualise dependencies regularly. A "core:common" module that everything depends on becomes a bottleneck.

Granularity is a trade-off

Too few modules give little benefit. Too many give slow syncs and ceremony. Feature-sized modules are a good default.

When it pays off

SituationRecommendation
Solo developer, small appSingle module with clear packages
Growing app, slow incremental buildsExtract core modules, then the largest features
Several teamsFeature modules with owners; convention plugins; dependency rules in CI
Planning KMPPure-Kotlin domain and data modules that can become multiplatform

Frequently asked questions

Does modularization always make builds faster?

Incremental builds and CI with caching usually improve, but configuration and sync times can grow with the number of modules. Measure before and after.

Should each layer be its own module?

Not necessarily. Many teams split by feature first, and add layer modules (domain, data) where boundaries need enforcing.

How do I wire DI across modules?

Each module contributes Hilt modules installed in components, and the app module brings them together automatically through Hilt's aggregation.

Conclusion

Multi-module architecture trades build configuration effort for faster incremental builds, enforced boundaries and team ownership. Start simple, extract core modules first, use implementation by default and convention plugins for consistency, and never let features depend on each other.

Next step: choose how to slice the app in Modularization Strategies for Large Android Apps.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

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.

Share: LinkedIn X / Twitter WhatsApp