Gradle build cache for a monorepo: what a multi-module run shows
A monorepo is many Gradle modules in one build. A shared cache server lets CI and every developer reuse each module's work. Below is a real run with four modules against FosterStack Cache 0.2.1: which modules came from the cache, and which did not.
The short answer
- Change one module's code and the other modules still come from the cache.
- Change a module's public methods and every module that uses it rebuilds.
- A teammate who makes the same change gets everything from the cache.
What we ran
A build with four modules. core has no dependencies. util and api use core. app uses util and api. Each module has 12 small Java classes. The cache server was release 0.2.1 (macOS, arm64), with Gradle 9.8.0.
Every run below used a brand-new copy of the project and an empty Gradle home folder, and the local cache was switched off. The only thing the runs shared was the cache server. Each run was gradle build --console=plain --no-daemon, and every build finished with BUILD SUCCESSFUL.
Results: what each module's compile step did
| Run | core | util | api | app |
|---|---|---|---|---|
| 1. First build, as CI would do it | ran | ran | ran | ran |
| 2. Another machine, same code | from cache | from cache | from cache | from cache |
3. Edit inside a method in core | ran | from cache | from cache | from cache |
| 4. A teammate makes the same edit | from cache | from cache | from cache | from cache |
5. Add a public method to core | ran | ran | ran | ran |
6. Edit only app | from cache | from cache | from cache | ran |
"Ran" means Gradle did the work and uploaded the result. "From cache" means Gradle downloaded it instead, and printed FROM-CACHE next to the task. This is what run 3 printed:
> Task :core:compileJava
> Task :util:compileJava FROM-CACHE
> Task :api:compileJava FROM-CACHE
> Task :app:compileJava FROM-CACHE
What this means for your monorepo
- Edits inside a method stay local (run 3). Gradle compares the public shape of a module, not every byte, when it decides whether a module that depends on it must rebuild. A change inside a method body left the other three modules' inputs the same, so they came from the cache.
- Changes to public methods spread (run 5). All four modules ran. In a big monorepo, a small, stable shared module with few public changes keeps more of the build cached.
- The first person pays, everyone else downloads (runs 4 and 6). The edit in run 3 was uploaded once. In run 4 a fresh checkout made the same edit and ran nothing.
Only the compile step is shown here, because it is the step that showed the pattern clearly in a small project. The jar steps in this build are not cacheable by default in Gradle, so they ran every time. The project is small, so we are not quoting time saved: the cache pays off as your modules get bigger.
Set it up
This is the exact settings.gradle.kts we ran. Replace the address with your server's. We used 127.0.0.1 over plain HTTP because the test ran on one machine; use HTTPS across a network.
buildCache {
local { isEnabled = false }
remote<HttpBuildCache> {
url = uri("http://127.0.0.1:18140/")
isPush = true
isAllowInsecureProtocol = true
}
}
And this is the whole gradle.properties:
org.gradle.caching=true
org.gradle.parallel=true
In a real team, let only CI upload (isPush) and have developers read. That setup, with a read-only login, is on the CI writes, developers read page.
If you do not see FROM-CACHE
Work through why the hit rate is low. To check the server itself, see your first 15 minutes, and for logins see authentication.
Run on Gradle 9.8.0 with FosterStack Cache 0.2.1 on macOS (arm64), Java 27.