Chapter 16Lesson 05~220 minutes

Checkpoint Lab — Groovy DSL and Kotlin DSL, Properties, Providers, Lazy Configuration, and Build Authoring

Author a small Gradle build, port one focused section between Kotlin and Groovy DSL, replace eager APIs with Providers and task registration, then prove configuration/execution behavior and missing-property handling from observable evidence.

CheckpointProviderTask registrationProperty validationRollback

Author a small Gradle build, port one focused section between Kotlin and Groovy DSL, replace eager APIs with Providers and task registration, then prove configuration/execution behavior and missing-property handling from observable evidence.

Learning objectives

  • Build a disposable Kotlin-DSL project through the verified Gradle 9.7.1 Wrapper and isolated Gradle User Home.
  • Predict which configuration/task/Provider/execution messages should appear for help versus a selected custom task.
  • Port one focused Provider/task section to Groovy DSL and verify equivalent output.
  • Replace one eager Provider/task lookup with orElse/map and tasks.register/named.
  • Implement both a safe fallback property and a clear task-specific missing-property error.
  • Verify outputs, optional configuration-cache reuse, and precise cleanup without touching normal Gradle state.

1. Checkpoint acceptance contract

Success is not “both files compile.” Your dossier must prove:

  • Gradle 9.7.1 Wrapper/runtime identity and isolated User Home.
  • Configuration messages are distinguishable from selected-task configuration and execution messages.
  • An unrelated task does not realize the custom task or require its strict property.
  • A focused Kotlin task and Groovy port produce equivalent output from the same property source.
  • The eager version causes observable extra coupling; the repaired version removes it.
  • Missing values have explicit fallback/error behavior without printing secrets.
  • Cleanup removes only the synthetic lab.

2. Setup and preflight

Start from the verified Wrapper files created in Chapter 15. Use no production credentials and no global init scripts.

mkdir gradle-authoring-checkpoint
cd gradle-authoring-checkpoint
export GRADLE_USER_HOME="$PWD/.checkpoint-gradle-home"

# Copy the already-verified Gradle 9.7.1 Wrapper files here.
chmod +x gradlew
./gradlew --version | tee wrapper-version.txt
java -version 2>&1 | tee java-version.txt

mkdir -p kotlin-build groovy-port

Expected chapter baseline: Gradle 9.7.1, JDK 21 runtime (or another supported runtime), Java 17 if you later add Java source. This checkpoint needs only Gradle core APIs, so it has no external plugin dependency.

3. Author the Kotlin DSL build with fallback and strict properties

cat > kotlin-build/settings.gradle.kts <<'EOF'
rootProject.name = "authoring-checkpoint"
EOF
cat > kotlin-build/gradle.properties <<'EOF'
academyAudience=repository-default
EOF
cat > kotlin-build/build.gradle.kts <<'EOF'
println("CONFIG: build script")

val audience = providers.gradleProperty("academyAudience")
    .orElse("local-fallback")
    .map {
        println("PROVIDER: audience realized")
        it.trim()
    }

val strictTier = providers.gradleProperty("deploymentTier")

val render = tasks.register("render") {
    println("TASK-CONFIG: render")
    val output = layout.buildDirectory.file("checkpoint/message.txt")
    outputs.file(output)
    doLast {
        val file = output.get().asFile
        file.parentFile.mkdirs()
        file.writeText("audience=${audience.get()}" + System.lineSeparator())
        println("EXECUTION: render wrote message.txt")
    }
}

tasks.register("verifyRender") {
    dependsOn(render)
    doLast {
        val file = layout.buildDirectory.file("checkpoint/message.txt").get().asFile
        check(file.readText().startsWith("audience="))
        println("EXECUTION: verifyRender passed")
    }
}

tasks.register("strictDeployCheck") {
    doLast {
        val tier = strictTier.orNull
            ?: throw GradleException(
                "strictDeployCheck requires -PdeploymentTier=<dev|stage|prod>"
            )
        check(tier in setOf("dev", "stage", "prod")) {
            "deploymentTier must be dev, stage, or prod"
        }
        println("EXECUTION: deployment tier validated; value not echoed")
    }
}
EOF

The audience has a safe non-secret fallback. The deployment tier is intentionally mandatory, but only for strictDeployCheck. The task validates allowed values without echoing the actual value.

4. Predict two state changes before running anything

Write your predictions into predictions.txt before running Gradle:

Prediction A — ./gradlew -p kotlin-build help
- CONFIG message: yes on a normal non-cached configuration
- TASK-CONFIG render: no
- PROVIDER audience realized: no
- build/checkpoint/message.txt: absent

Prediction B — ./gradlew -p kotlin-build verifyRender
- render task must be configured and executed
- audience Provider must be realized when render action needs it
- message.txt must exist before verifyRender completes

The point is causal accountability: if reality differs, investigate which API forced realization.

5. Prove configuration versus execution behavior

./gradlew -p kotlin-build help --console=plain | tee help.log
./gradlew -p kotlin-build verifyRender --console=plain | tee verify.log

printf '%s
' '--- help phase messages ---'
grep -E 'CONFIG:|TASK-CONFIG:|PROVIDER:|EXECUTION:' help.log || true
printf '%s
' '--- verify phase messages ---'
grep -E 'CONFIG:|TASK-CONFIG:|PROVIDER:|EXECUTION:' verify.log || true

cat kotlin-build/build/checkpoint/message.txt

Expected message content is audience=repository-default unless a higher-priority source was supplied. help should not create the output file or need the strict deployment property.

6. Override the fallback/default through a controlled high-priority source

./gradlew -p kotlin-build render --rerun-tasks   -PacademyAudience=checkpoint-cli --console=plain
cat kotlin-build/build/checkpoint/message.txt
# Expected: audience=checkpoint-cli

This verifies property wiring without editing the build script. Record the command as evidence because the command-line override is part of the effective build input.

7. Port the focused render behavior to Groovy DSL

Create a separate Groovy project. Port only the property + render task, not the entire checkpoint:

cat > groovy-port/settings.gradle <<'EOF'
rootProject.name = 'authoring-checkpoint-groovy'
EOF
cat > groovy-port/gradle.properties <<'EOF'
academyAudience=repository-default
EOF
cat > groovy-port/build.gradle <<'EOF'
println 'CONFIG: Groovy build script'

def audience = providers.gradleProperty('academyAudience')
    .orElse('local-fallback')
    .map {
        println 'PROVIDER: Groovy audience realized'
        it.trim()
    }

tasks.register('render') {
    println 'TASK-CONFIG: Groovy render'
    def output = layout.buildDirectory.file('checkpoint/message.txt')
    outputs.file(output)
    doLast {
        def file = output.get().asFile
        file.parentFile.mkdirs()
        file.text = "audience=${audience.get()}" + System.lineSeparator()
        println 'EXECUTION: Groovy render wrote message.txt'
    }
}
EOF

./gradlew -p groovy-port render -PacademyAudience=checkpoint-cli   --console=plain | tee groovy.log
cat groovy-port/build/checkpoint/message.txt

diff -u kotlin-build/build/checkpoint/message.txt         groovy-port/build/checkpoint/message.txt

A zero diff is stronger evidence than “the Groovy code looks equivalent.” Both builds used the same Wrapper and effective project-property value and produced the same focused output.

8. Inject one eager lookup and diagnose the coupling

Temporarily add this to the top of kotlin-build/build.gradle.kts:

// INTENTIONALLY BROKEN FOR THE CHECKPOINT
val eagerStrictTier = providers.gradleProperty("deploymentTier").get()
val eagerRender = tasks.getByName("render")
./gradlew -p kotlin-build help --console=plain | tee eager-help.log || true

Expected: help can now fail for missing deploymentTier, and getByName("render") forces the render task to realize even though help does not need it. Preserve eager-help.log, then remove both lines.

The repair is not “set deploymentTier globally.” The repair is the Provider + tasks.register design already present in the good version.

9. Inject a missing property and prove narrow error handling

./gradlew -p kotlin-build help --console=plain
# Must succeed without deploymentTier.

./gradlew -p kotlin-build strictDeployCheck --console=plain || true
# Expected clear task-specific error: property required.

./gradlew -p kotlin-build strictDeployCheck   -PdeploymentTier=stage --console=plain
# Expected: validation succeeds without echoing the value.

This is the required distinction between a fallback and an error. academyAudience has a documented safe fallback. deploymentTier has no fallback because silently choosing a deployment tier would be dangerous; it fails only at the task that needs it.

10. Optional configuration-cache evidence

After the eager lines are removed, run:

./gradlew -p kotlin-build help --configuration-cache --console=plain
./gradlew -p kotlin-build help --configuration-cache --console=plain

If the second run reuses the configuration cache, top-level configuration output may not repeat. That is expected: replaying cached configured state is not the same as re-evaluating the build script. If it does not reuse, inspect Gradle’s stated reason rather than forcing reuse.

11. Verification checklist

  • Wrapper/runtime identity was captured.
  • All mutable Gradle user state stayed under .checkpoint-gradle-home.
  • help did not need render, its Provider, or deploymentTier after repair.
  • Kotlin and Groovy focused outputs matched exactly.
  • -PacademyAudience=... overrode the committed root default.
  • The intentionally eager variant produced preserved diagnostic evidence.
  • The strict property failed only its consuming task and validated allowed values without printing the value.
  • No normal user cache or production secret was touched.

12. Cleanup and rollback

./gradlew --stop || true
cd ..
rm -rf gradle-authoring-checkpoint

The experiment leaves no global property, init script, cache deletion, or Wrapper upgrade behind. In a real repository, rollback would mean reverting the reviewed build-script change while preserving logs/evidence from CI.

13. What Chapter 16 adds to the production build-engineering model

You can now trace Gradle build authoring from script scope to model object to Provider/task handle to execution and output. You can also distinguish repository defaults from user/CI overrides, explain Kotlin accessor timing, and diagnose when configuration-time work creates unnecessary coupling.

Chapter 17 builds directly on this foundation: tasks become a graph with dependencies and ordering rules, inputs and outputs become explicit incremental-work contracts, and task outcomes become evidence you can reason about rather than merely commands you run.

Knowledge check

Why is the Kotlin/Groovy diff useful?

What does the eager deploymentTier.get() demonstrate?

Why is tasks.getByName("render") included in the broken variant?

Why does academyAudience get a fallback while deploymentTier does not?

If Configuration Cache reuses the second help invocation and the CONFIG print disappears, is the build broken?

What conceptual bridge leads to Chapter 17?

Official references and version notes

Version snapshot: Generated for August 24, 2026 with Gradle 9.7.1, JDK 21 as the Gradle runtime, and Java 17 as the small JVM-project target. Re-check current Gradle documentation before carrying version-sensitive recommendations into future production builds.

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.