Infrastructure worth keeping — and kept patched.

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

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

Runcoreutilapiapp
1. First build, as CI would do itranranranran
2. Another machine, same codefrom cachefrom cachefrom cachefrom cache
3. Edit inside a method in coreranfrom cachefrom cachefrom cache
4. A teammate makes the same editfrom cachefrom cachefrom cachefrom cache
5. Add a public method to coreranranranran
6. Edit only appfrom cachefrom cachefrom cacheran

"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

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.