Infrastructure worth keeping — and kept patched.

Gradle build cache: monorepo, buildSrc and included builds

In a big Gradle repo, an edit should rebuild only the part you edited. FosterStack Cache is a shared cache server, so in our runs the parts you did not change came from the cache.

In our runs, a module, buildSrc or an included build came from the cache unless its own code changed, or something it uses changed its public methods.

The short answer

Our advice, not a test result

Set it up

This is the exact settings.gradle.kts we ran for the four-module build. 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

What a multi-module run shows

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 table shows 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

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.

Does Gradle cache buildSrc and included builds?

Yes, in our runs. The build had three parts: a buildSrc folder with one Java class, an included build called lib with one Java class, and an app project that uses lib. The columns show the compileJava task of each part.

RunbuildSrclib (included build)app
1. First build, as CI would do itranranran
2. Fresh copy, same codeFROM-CACHEFROM-CACHEFROM-CACHE
3. Edit buildSrcranFROM-CACHEFROM-CACHE
4. Edit inside a method in libFROM-CACHEranFROM-CACHE
5. Add a public method to libFROM-CACHEranran
6. Edit only appFROM-CACHEFROM-CACHEran

These are three lines from run 2, with the lines between them left out:

> Task :buildSrc:compileJava FROM-CACHE
...
> Task :lib:compileJava FROM-CACHE
...
> Task :app:compileJava FROM-CACHE

We tried one small class in each part. We did not test a buildSrc that holds plugins, convention plugins, or other languages, and we did not test an included build that is itself a large multi-module build.

The files we used. Root settings.gradle.kts, with the address for a test on one machine over plain HTTP; use your server's address and HTTPS across a network:

rootProject.name = "composite"
includeBuild("lib")
include(":app")

buildCache {
    local { isEnabled = false }
    remote<HttpBuildCache> {
        url = uri("http://127.0.0.1:18220/")
        isPush = true
        isAllowInsecureProtocol = true
    }
}

The root gradle.properties has one line, org.gradle.caching=true. The root build.gradle.kts has one line that prints a value from the buildSrc class. lib/settings.gradle.kts is rootProject.name = "lib". buildSrc/build.gradle.kts is plugins { java }. lib/build.gradle.kts is plugins { `java-library` } with group = "demo", and app/build.gradle.kts adds implementation("demo:lib").

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, for logins see authentication, and to see what Gradle stores, see what Gradle stores in the build cache.

How we checked

All the runs above used Gradle 9.8.0 with FosterStack Cache 0.2.1 on macOS (arm64), Java 27. Every run used its own copy of the project folder, an empty Gradle home folder, and the local cache switched off. The only Gradle state the runs shared was the cache server. Every build finished with BUILD SUCCESSFUL.

Four-module build: six runs, each gradle build --console=plain --no-daemon. An earlier set of runs started from a project folder that still held build output left over from an earlier test. We repeated all six runs from a clean copy and got the same results, down to the same entry count on the server. The table shows the clean runs.

buildSrc and included build: six runs, each gradle :app:build --console=plain --no-daemon, with Java 27 from Homebrew and the same Gradle install and Java for every run. Runs 3 to 6 each made one edit on a fresh copy of the original code; the edits were not stacked.

The runs above were on a Mac with 0.2.1. We ran both tables again on a GitHub-hosted Linux machine with FosterStack Cache 0.2.2, Gradle 9.8.0 and Java 21, each run on a fresh copy of the project and with an empty Gradle home. Every cell of both tables came out the same, and in the four-module build the jar steps ran every time. Run 5 (“all four ran”) only happens when app can see core’s public code. In the project we built for this run, util and api are java-library modules that each declare api(project(":core")), so app sees core through them. The run, the build files and the log are public.