android

Gradle’s ABI Hash Trick: Why One Edit Skips 40 Modules

Most Android engineers have watched Gradle skip forty modules after editing one and wondered how it decided. The answer is not magic. It is a hash.

Gradle uses a process called ABI hashing to check whether a module's public surface has changed, rebuilding only the dependents that need it. Understanding the mechanism is one of those quiet skills that separates senior Android developers from the rest of the team.

At a glance:

  • Gradle compiles Kotlin source to JVM bytecode before hashing anything
  • The ASM library strips private internals, leaving only the public ABI
  • A SHA-256 hash of that stripped skeleton is stored in the local build cache
  • If the new hash matches the old one, Gradle skips recompilation of every dependent module
  • Changing a public function signature breaks the hash and forces a rebuild

What an ABI actually is

ABI stands for binary application interface. For a Kotlin module, it is every public class, function signature, property type, interface, and superclass that other modules can see. Private function bodies, local variables, comments, and annotations on private members are not part of the ABI.

The rule Gradle follows is simple: if the public surface did not change, no dependent module needs to know. Private internals are invisible to consumers, so editing them is free.

How the hashing works, step by step

The Kotlin compiler turns `.kt` files into `.class` bytecode. Gradle then hands that bytecode to ASM, a library that reads and rewrites class files without running them. ASM acts as an ABI extractor and strips anything that is not public, leaving a skeleton made of signatures only.

A SHA-256 hash is calculated on that skeleton and stored in `~/.gradle/caches/`. On the next build, Gradle recomputes the hash and compares it to the saved value. Match, and the module plus everything that depends on it is skipped. Mismatch, and the dependents get recompiled.

A payments module, walked through

Say you maintain a module called `:payments-data`. Its `PaymentRepository` exposes one public function, `processPayment(amount: Double): PaymentResult`. The body calls two private helpers, `calculateFee` and `executeCharge`. ASM keeps the public signature and drops both private helpers from the skeleton. The resulting hash is something like `a3f8c2d9e1b4…`.

You swap your payment gateway from v1 to v2 by editing only the private `executeCharge` body. ASM rebuilds the skeleton. The public signature is unchanged, the hash is unchanged, and Gradle skips every module that touches `:payments-data`. A build that would have taken several minutes finishes in seconds.

Now add a `currency: String` parameter to `processPayment`. The skeleton changes, the hash changes, and every dependent—including `:presentation`, `:domain`, and `:feature-checkout`—gets recompiled.

Why it matters

This mechanism is a direct performance lever for any team with more than a handful of modules. The insight is structural: private edits are free, public edits are expensive, and the hash is what draws that line.

For engineers who understand it, refactoring private code loses its fear factor. You know the blast radius is contained. You also become more deliberate about public APIs, because every public change forces a rebuild across the dependency graph.

The trade-off lives in module design. Tight, well-scoped public surfaces are cheap to maintain. Bloated ones, full of public helpers that should have been internal, tax the team on every commit. Treat the public surface like a product: name it carefully, resist adding to it without reason, and default to internal visibility.

Change type Effect on ABI hash Rebuild scope
Edit private function body None Local module only
Rename private function None Local module only
Add public parameter Breaks hash All dependents
Change public return type Breaks hash All dependents

What to watch next

Gradle's ongoing push toward configuration caching and project isolation both build on ABI hashing. Also watch Kotlin's K2 compiler—it may change how the ABI skeleton gets generated, with implications for hash stability across compiler versions.

Bottom line: private edits are free, public edits are expensive, and the hash is what tells Gradle which one just happened.

Who should care

  • Senior Android engineers** managing multi-module architectures—ABIs are a first-class design concern
  • Teams with 10+ modules**—every public function added to a core module is a future build-time tax
  • Engineers doing cross-module refactoring**—understanding ABI boundaries prevents accidental forced rebuilds
  • Build infrastructure maintainers**—remote cache hit rates depend directly on stable ABI hashes

Key takeaways

  • ABI hashing only inspects the public surface; private edits never trigger dependent rebuilds
  • ASM strips private internals before SHA-256 runs, keeping the hash stable for internal changes
  • The cache lives in `~/.gradle/caches/`, and a match means skip
  • Small, stable module APIs are a direct build-performance win
  • Treat every new public function as a future multi-minute build tax

Frequently asked questions

Does ABI hashing work with Java modules too?

Yes. The same ASM-based extraction runs on JVM bytecode, regardless of whether the source is Kotlin or Java.

Will renaming a private function break the hash?

No. Private members are stripped before the hash is calculated, so their names are not in the skeleton.

Where is the cached hash stored?

Inside Gradle's local build cache, typically under `~/.gradle/caches/`.

Does this help with KMP or multiplatform projects?

It helps on the JVM side. The non-JVM targets have their own compilation rules and do not use the same ASM-based extraction.

Related