โš™๏ธ Advanced Android Engineering and Emerging Technology

Multi-Module Gradle Builds and Build Optimization

Slow builds tax every developer, every day. In multi-module Android projects, a handful of Gradle settings and structural choices often cut build times dramatically. Here's how to measure, what to enable, and which module design choices matter.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Advanced
On this page
  1. Where build time goes
  2. Measure first
  3. High-impact settings
  4. Prefer KSP over kapt
  5. Module structure choices
  6. Build script hygiene
  7. CI builds
  8. Typical wins
  9. Frequently asked questions
  10. Conclusion

A 3-minute incremental build, run 40 times a day by 20 developers, is 40 developer-hours lost daily. Build speed is productivity. Most wins come from measuring, enabling caches, and avoiding module structures that force unnecessary recompilation.

Where build time goes

Gradle build phases and caches Initialization determines projects; configuration evaluates build scripts, accelerated by the configuration cache; execution runs tasks, accelerated by up-to-date checks, the local and remote build cache and parallel execution. Initializationwhich projects Configurationโ†’ configuration cache Executionโ†’ build cache, parallel Measure each phase before changing anything (build scans, Build Analyzer).
Configuration time grows with module count. Execution time grows with what needs recompiling.

Measure first

  • Build Analyzer in Android Studio identifies slow tasks and plugins.
  • Gradle build scans (--scan) show timelines, cache hits and configuration time.
  • Benchmark typical scenarios: clean build, an ABI change in a core module, a non-ABI change in a feature module.

High-impact settings

gradle.propertiesProperties
# Reuse configuration results between builds
org.gradle.configuration-cache=true
# Reuse task outputs from previous builds (local, optionally remote)
org.gradle.caching=true
# Run independent module tasks in parallel
org.gradle.parallel=true
# Give the daemon enough memory (tune for your machine)
org.gradle.jvmargs=-Xmx6g -XX:+UseParallelGC -Dfile.encoding=UTF-8
# Smaller, non-transitive R classes: changes in one module's resources recompile less
android.nonTransitiveRClass=true

Recent AGP versions enable some of these by default (non-transitive R classes, for example). Check what your versions default to, and verify that plugins are configuration-cache compatible.

Prefer KSP over kapt

kapt generates Java stubs and is significantly slower. Hilt, Room and Moshi support KSP. Migrating annotation processors to KSP often gives large wins in modules that use them.

Module structure choices

  • implementation over api: limits recompilation of consumers when internals change.
  • API/impl splits for frequently changed features used by others.
  • Avoid a giant "common" module that everything depends on: one change rebuilds the world.
  • Pure Kotlin/JVM modules for domain logic. They build and test faster than Android library modules.
  • Convention plugins instead of subprojects { } or allprojects { }, which slow configuration and break isolation.

Build script hygiene

Avoid eager, configuration-time workGradle KTS
// โŒ Runs at configuration time on EVERY build (even unrelated tasks)
val gitSha = Runtime.getRuntime().exec("git rev-parse --short HEAD").inputStream.reader().readText().trim()

// โœ… Lazy provider evaluated only when needed (configuration-cache friendly)
val gitSha = providers.exec { commandLine("git", "rev-parse", "--short", "HEAD") }
    .standardOutput.asText.map { it.trim() }
  • Use lazy APIs (register, providers), not create, and don't read files at configuration time.
  • Disable unused build features per module (buildConfig, aidl, renderscript).
  • Keep dependencies and plugins up to date. Gradle and AGP improve performance regularly.

CI builds

  • Cache Gradle home and the build cache between runs (for example with gradle/actions/setup-gradle).
  • Consider a remote build cache shared by CI and developers, populated by trusted CI builds.
  • Run only affected modules' tests on PRs in large projects.

Typical wins

kapt โ†’ KSP

Large reductions in annotation-processing time for Room and Hilt modules.

Configuration cache

Repeat builds skip configuration, especially noticeable with many modules.

Breaking up "core:common"

Fewer modules recompiled per change, and better parallelism.

Frequently asked questions

Do more modules always mean faster builds?

Not always. Incremental builds and parallelism improve, but configuration time and sync time grow. Measure to find the sweet spot.

Why is my configuration cache failing?

A plugin or script does configuration-time work that isn't cacheable. Build output reports the problem, so update or replace the plugin, or rewrite the logic lazily.

Does more RAM help?

Up to a point. Too little memory causes GC thrashing. Tune org.gradle.jvmargs and the Kotlin daemon settings, and monitor them.

Conclusion

Fast Android builds come from measuring, enabling configuration and build caches, parallel execution, KSP, non-transitive R classes, sensible module boundaries with implementation dependencies, and lazy build scripts, plus caching in CI.

Next step: decide how far to share code across platforms with Compose Multiplatform vs Kotlin Multiplatform.

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?

Performance work starts with understanding how Android apps are built and how they behave at runtime. Strengthen those fundamentals and the profiling tools start to make sense.

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