Chapter 24Lesson 02~310 minutes

Incremental Builds, Configuration Cache, Build Cache, Remote Cache, and Reproducibility: Guided Hands-On Workflow and Core Operations

Use a disposable Java build to observe UP-TO-DATE versus FROM-CACHE, opt into the configuration cache, compare clean workspaces, and prove what invalidates each layer.

Gradle 9.7.1Local cacheConfiguration cacheSHA-256Clean workspaces

Learning objectives

  • Create a disposable Java build using the verified Gradle 9.7.1 Wrapper and an isolated Gradle User Home.
  • Observe the same task as executed, UP-TO-DATE, and FROM-CACHE.
  • Opt into the Configuration Cache and distinguish store/reuse messages from task-output cache outcomes.
  • Compare JAR SHA-256 values from separate checkout paths and explain what that does and does not prove.
  • Use a shared directory cache as a local simulation of cross-agent output reuse without configuring an HTTP service.

1. Preflight and lab boundary

This lab assumes the verified Gradle 9.7.1 Wrapper from Chapter 15 is available. Do not run a random system gradle binary. Gradle runs on JDK 21 and compiles the tiny library with a Java 21 toolchain plus Java 17 --release target. No external application dependency is needed.

set -eu
java -version
./gradlew --version

# Keep all mutable Gradle state for this chapter under one disposable parent.
LAB="$PWD/gradle-cache-lab"
mkdir -p "$LAB/template/src/main/java/dev/academy/cache" "$LAB/template/src/greeting"
export GRADLE_USER_HOME="$LAB/gradle-user-home"
printf 'GRADLE_USER_HOME=%s\n' "$GRADLE_USER_HOME"

Wrapper trust remains in force. Use the wrapper files already reviewed in Chapter 15. Do not regenerate or retarget the Wrapper merely for this cache lab, and do not weaken distribution checksum verification.

2. Create the deterministic project

The task model contains one cacheable custom task. Its template file uses relative path sensitivity, its audience is a declared scalar input, and its output is a project build file. The Java JAR is configured with Gradle 9+ reproducible archive settings explicitly even though those values are current defaults.

rootProject.name = "cache-lab"

// The normal local Build Cache lives under GRADLE_USER_HOME. For this lab we
// redirect it to one disposable directory shared by workspace-a/workspace-b.
buildCache {
    local {
        directory = rootDir.parentFile.resolve("shared-build-cache")
        enabled = true
        push = true
    }
}
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.bundling.AbstractArchiveTask

plugins {
    `java-library`
}

group = "dev.academy.cache"
version = "1.0.0"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.withType<JavaCompile>().configureEach {
    options.release.set(17)
}

// These are already Gradle 9+ archive defaults; writing them here makes the
// reproducibility contract visible to the learner and reviewers.
tasks.withType<AbstractArchiveTask>().configureEach {
    isPreserveFileTimestamps = false
    isReproducibleFileOrder = true
}

@CacheableTask
abstract class RenderGreeting : DefaultTask() {
    @get:InputFile
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val templateFile: RegularFileProperty

    @get:Input
    abstract val audience: Property<String>

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun render() {
        val text = templateFile.get().asFile.readText()
            .replace("{{audience}}", audience.get())
        val out = outputFile.get().asFile
        out.parentFile.mkdirs()
        out.writeText(text)
    }
}

tasks.register<RenderGreeting>("renderGreeting") {
    templateFile.set(layout.projectDirectory.file("src/greeting/template.txt"))
    audience.convention(providers.gradleProperty("audience").orElse("engineers"))
    outputFile.set(layout.buildDirectory.file("generated/greeting.txt"))
}
package dev.academy.cache;

public final class Greeting {
    private Greeting() {}

    public static String value(String name) {
        String safe = (name == null || name.isBlank()) ? "world" : name.strip();
        return "hello, " + safe;
    }
}
# src/greeting/template.txt
Welcome, {{audience}}.

# gradle.properties
org.gradle.caching=false
org.gradle.configuration-cache=false
cd "$LAB/template"
printf '%s
' 'rootProject.name = "cache-lab"' > /dev/null
# Save the shown settings.gradle.kts and build.gradle.kts exactly as above.
# Save Greeting.java and src/greeting/template.txt exactly as above.
# Copy the already verified gradlew, gradlew.bat, and gradle/wrapper/ directory here.
chmod +x gradlew 2>/dev/null || true
./gradlew --version

The org.gradle.* properties begin false so the first observations are not ambiguous. The shared cache directory is configured, but Gradle does not consult it until build caching is enabled.

3. Observe ordinary execution and UP-TO-DATE

cd "$LAB/template"
./gradlew clean renderGreeting jar --no-build-cache --no-configuration-cache --console=plain | tee ../01-first.log
./gradlew renderGreeting jar --no-build-cache --no-configuration-cache --console=plain | tee ../02-repeat.log

cat build/generated/greeting.txt
sha256sum build/libs/cache-lab-1.0.0.jar | tee ../jar-in-place.sha256

On the first run, work executes because outputs do not exist. On the immediate repeat, correctly modeled tasks can report UP-TO-DATE. Nothing was downloaded from a task-output cache; Gradle simply compared current declared state with outputs already present in this workspace.

4. Enable the Build Cache for one invocation

cd "$LAB/template"
./gradlew clean renderGreeting jar --build-cache --no-configuration-cache --console=plain | tee ../03-cache-store.log

# clean removes project outputs, not the shared Build Cache.
./gradlew clean
./gradlew renderGreeting jar --build-cache --no-configuration-cache --console=plain | tee ../04-cache-restore.log

grep -E 'FROM-CACHE|UP-TO-DATE' ../04-cache-restore.log || true

The expected distinction is now observable: after clean, a cacheable task may report FROM-CACHE because its outputs were restored from the shared directory cache. If a task executes instead, that is evidence to inspect cacheability and key inputs—not a reason to assume the feature is broken.

5. Opt into Configuration Cache separately

cd "$LAB/template"
./gradlew renderGreeting --configuration-cache --no-build-cache --console=plain | tee ../05-cc-store.log
./gradlew renderGreeting --configuration-cache --no-build-cache --console=plain | tee ../06-cc-reuse.log

# Inspect only paths/messages; do not dump binary configuration-cache entries.
grep -Ei 'configuration cache' ../05-cc-store.log ../06-cc-reuse.log || true
find .gradle/configuration-cache -type f 2>/dev/null | head -n 20 || true

The first compatible run should store a configuration-cache entry; a later compatible invocation can reuse it. This says nothing about whether renderGreeting executes, is up-to-date, or comes from the Build Cache. Configuration reuse and task-output reuse remain orthogonal.

6. Use both caches and read the evidence correctly

cd "$LAB/template"
./gradlew clean renderGreeting jar --build-cache --configuration-cache --console=plain | tee ../07-both.log
grep -E 'FROM-CACHE|UP-TO-DATE|Configuration cache|configuration cache' ../07-both.log || true

A single build can reuse configuration and restore some task outputs from the Build Cache. Record both kinds of evidence. Do not collapse the summary into “Gradle used cache,” because that loses which state was reused and why.

7. Simulate cross-agent reuse with two project paths

The built-in local cache can point at a directory. We use one disposable directory outside both workspaces so workspace B can reuse outputs produced in A. This simulates the cross-workspace behavior of shared cache reuse without claiming to test HTTP transport, authentication, TLS, or production cache governance.

cd "$LAB"
cp -R template workspace-a
cp -R template workspace-b
rm -rf workspace-a/.gradle workspace-a/build workspace-b/.gradle workspace-b/build

cd workspace-a
./gradlew renderGreeting jar --build-cache --no-configuration-cache --console=plain | tee ../08-a.log

cd ../workspace-b
./gradlew renderGreeting jar --build-cache --no-configuration-cache --console=plain | tee ../09-b.log
grep 'FROM-CACHE' ../09-b.log || true

Relative path sensitivity makes the custom task eligible for reuse across different checkout roots. Absolute path sensitivity would make the path itself part of the key and typically prevent this reuse.

8. Prove artifact bytes independently of output caches

cd "$LAB/workspace-a"
./gradlew clean jar --no-build-cache --no-configuration-cache --console=plain
sha256sum build/libs/cache-lab-1.0.0.jar > ../workspace-a.sha256

cd "$LAB/workspace-b"
./gradlew clean jar --no-build-cache --no-configuration-cache --console=plain
sha256sum build/libs/cache-lab-1.0.0.jar > ../workspace-b.sha256

cat ../workspace-a.sha256 ../workspace-b.sha256
cmp -s build/libs/cache-lab-1.0.0.jar ../workspace-a/build/libs/cache-lab-1.0.0.jar && echo 'byte-identical' || echo 'DIFFERENT'
jar tf build/libs/cache-lab-1.0.0.jar | sort | head -n 40

The final identity run disables both reuse mechanisms. If the JARs match byte-for-byte across separate paths, you have useful reproducibility evidence for this fixture. It is still scoped evidence: a larger build may include generators, native tools, timestamps, locale, or signing state that require additional controls.

9. Change one declared input and observe causality

cd "$LAB/workspace-b"
printf '%s
' 'Welcome to the cache lab, {{audience}}.' > src/greeting/template.txt
./gradlew renderGreeting --build-cache --configuration-cache --console=plain | tee ../10-input-change.log
cat build/generated/greeting.txt

The template is a declared task input, so the task output-cache key must change. The build-script task graph may still reuse configuration because editing a task input file does not necessarily change build configuration. This is the kind of layered reasoning Chapter 24 expects.

10. Challenge: choose the correct control

Symptom Choose first Why
Output file exists and task is skipped after no changes Inspect up-to-date inputs/outputs This is current-workspace incremental state, not necessarily the Build Cache.
After clean, output reappears without task execution Inspect Build Cache evidence/key clean removed outputs but not the cache entry.
Build skips configuration but task still runs Inspect Configuration Cache separately Configuration reuse does not imply task-output reuse.
Same source in two directories makes different JAR hashes Inspect reproducibility inputs/metadata Cache toggles cannot fix nondeterministic output generation.

11. Cleanup

cd "$LAB/.."
rm -rf gradle-cache-lab

Only the disposable lab, its isolated Gradle User Home, its shared-directory build cache, and project-local state are removed. Do not delete normal ~/.gradle or shared organization caches.

Knowledge check

Why run clean between Build Cache demonstrations?

What does a reused Configuration Cache entry prove about JAR bytes?

Why use --no-build-cache --no-configuration-cache for the final checksum comparison?

What does the shared directory cache simulate?

A declared template file changes. Which layer must invalidate the task result?

What should you do if workspace B executes instead of reporting FROM-CACHE?

Official references and version notes

Version-sensitive behavior was rechecked against current Gradle primary documentation on 2026-08-24. The mandatory path uses Gradle 9.7.1 through the previously verified Wrapper, JDK 21 as the Gradle runtime/compiler toolchain, Java 17 as the project target, isolated lab state, and no paid service. Both the Build Cache and Configuration Cache are opt-in in Gradle 9.7.1. The Configuration Cache is local-only and cannot currently be shared across developers or CI machines. Remote HTTP build-cache examples are explanatory only; the hands-on cross-workspace exercise uses a disposable shared directory cache so no server or credentials are needed.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.