Groovy DSL and Kotlin DSL, Properties, Providers, Lazy Configuration, and Build Authoring: Guided Hands-On Workflow and Core Operations
Author the same small Gradle behavior in Kotlin and Groovy DSL, prove project-property precedence, observe Provider realization, and migrate eager task/property lookups to lazy APIs without relying on hidden user state.
Author the same small Gradle behavior in Kotlin and Groovy DSL, prove project-property precedence, observe Provider realization, and migrate eager task/property lookups to lazy APIs without relying on hidden user state.
Learning objectives
- Create Kotlin-DSL and Groovy-DSL projects that use the same Wrapper and isolated Gradle User Home.
- Express equivalent Provider-based project properties and lazy tasks in both DSLs.
- Prove the project-property precedence order with controlled root, user-home, environment, system, and command-line values.
- Observe configuration, task configuration, Provider realization, and execution as separate messages.
-
Replace
tasks.create/getByNameand eager property lookup withregister/namedand Provider wiring. - Verify outputs and clean only disposable lab state.
1. Scenario: one Gradle engine, two DSL surfaces
You will create two tiny builds under one disposable directory:
kotlin-lab/ and groovy-lab/. Both use the
same already-verified Gradle 9.7.1 Wrapper files from Chapter 15 and
the same isolated GRADLE_USER_HOME. This keeps the
build engine constant while the DSL changes.
If you did not complete Chapter 15, bootstrap the Wrapper once with a trusted Gradle 9.7.1 installation and verify its distribution and Wrapper-JAR checksums before using it. Do not download an arbitrary Wrapper JAR.
2. Preflight: prove runtime and isolate mutable state
mkdir gradle-authoring-lab
cd gradle-authoring-lab
export GRADLE_USER_HOME="$PWD/.lab-gradle-home"
# Copy the verified Chapter 15 Wrapper files into this directory first:
# gradlew, gradlew.bat, gradle/wrapper/gradle-wrapper.jar,
# gradle/wrapper/gradle-wrapper.properties
chmod +x gradlew
./gradlew --version
java -version
Expected: Wrapper Gradle 9.7.1 and a supported runtime JVM (JDK 21
in this course baseline). The two child builds below are selected
with -p; they do not need duplicate Wrapper binaries.
3. Author the Kotlin DSL build with observable lazy behavior
mkdir -p kotlin-lab
cat > kotlin-lab/settings.gradle.kts <<'EOF'
rootProject.name = "kotlin-authoring-lab"
EOF
cat > kotlin-lab/gradle.properties <<'EOF'
academyAudience=repository-default
EOF
cat > kotlin-lab/build.gradle.kts <<'EOF'
println("CONFIG: Kotlin build script evaluated")
val audience = providers.gradleProperty("academyAudience")
.orElse("fallback-audience")
val message = audience.map {
println("PROVIDER: Kotlin message realized")
"hello-${it.trim()}"
}
val writeMessage = tasks.register("writeMessage") {
println("TASK-CONFIG: Kotlin writeMessage configured")
val outputFile = layout.buildDirectory.file("authoring/message.txt")
outputs.file(outputFile)
doLast {
val file = outputFile.get().asFile
file.parentFile.mkdirs()
file.writeText(message.get() + System.lineSeparator())
println("EXECUTION: wrote ${file.relativeTo(project.projectDir)}")
}
}
tasks.register("verifyMessage") {
dependsOn(writeMessage)
doLast {
check(layout.buildDirectory.file("authoring/message.txt").get().asFile.isFile)
println("EXECUTION: verified message file")
}
}
EOF
The top-level println belongs to configuration. The
print inside the task-registration action appears only if Gradle
realizes writeMessage. The print inside
map appears only when the message Provider is obtained.
The doLast messages are execution.
4. Run an unrelated task first: registered should remain unrealized
./gradlew -p kotlin-lab help --console=plain | tee kotlin-help.log
grep -E 'CONFIG:|TASK-CONFIG:|PROVIDER:|EXECUTION:' kotlin-help.log || true
Expected on a normal uncached run:
CONFIG: Kotlin build script evaluated appears. The
writeMessage configuration action, Provider
transformation, and task execution messages should not be required
by help. If they appear, inspect what forced
realization.
5. Prove property precedence without guessing
First add a user-home value. It should override the repository-root value:
cat > "$GRADLE_USER_HOME/gradle.properties" <<'EOF'
academyAudience=user-home
EOF
./gradlew -p kotlin-lab writeMessage --console=plain
cat kotlin-lab/build/authoring/message.txt
# Expected: hello-user-home
Now test higher-priority sources one at a time:
ORG_GRADLE_PROJECT_academyAudience=environment ./gradlew -p kotlin-lab writeMessage --rerun-tasks --console=plain
cat kotlin-lab/build/authoring/message.txt
# Expected: hello-environment
./gradlew -p kotlin-lab writeMessage --rerun-tasks -Dorg.gradle.project.academyAudience=system --console=plain
cat kotlin-lab/build/authoring/message.txt
# Expected: hello-system
./gradlew -p kotlin-lab writeMessage --rerun-tasks -PacademyAudience=cli --console=plain
cat kotlin-lab/build/authoring/message.txt
# Expected: hello-cli
You changed only the source of one project property. The task model and output path stayed the same; the Provider resolved a different value according to the documented priority.
6. Port the focused behavior to Groovy DSL, preserving the model
mkdir -p groovy-lab
cat > groovy-lab/settings.gradle <<'EOF'
rootProject.name = 'groovy-authoring-lab'
EOF
cat > groovy-lab/gradle.properties <<'EOF'
academyAudience=repository-default
EOF
cat > groovy-lab/build.gradle <<'EOF'
println 'CONFIG: Groovy build script evaluated'
def audience = providers.gradleProperty('academyAudience')
.orElse('fallback-audience')
def message = audience.map {
println 'PROVIDER: Groovy message realized'
"hello-${it.trim()}"
}
def writeMessage = tasks.register('writeMessage') {
println 'TASK-CONFIG: Groovy writeMessage configured'
def outputFile = layout.buildDirectory.file('authoring/message.txt')
outputs.file(outputFile)
doLast {
def file = outputFile.get().asFile
file.parentFile.mkdirs()
file.text = message.get() + System.lineSeparator()
println "EXECUTION: wrote ${project.relativePath(file)}"
}
}
tasks.register('verifyMessage') {
dependsOn writeMessage
doLast {
assert layout.buildDirectory.file('authoring/message.txt').get().asFile.isFile()
println 'EXECUTION: verified message file'
}
}
EOF
./gradlew -p groovy-lab verifyMessage -PacademyAudience=cli --console=plain
cat groovy-lab/build/authoring/message.txt
The observable model should match the Kotlin project: same project property, same Provider transformation, same lazy registration, same task dependency, same output path. The code does not need to be textually identical to be semantically equivalent.
7. Inspect when the task and Provider are realized
./gradlew -p kotlin-lab writeMessage -PacademyAudience=trace --rerun-tasks --console=plain | tee kotlin-write.log
grep -E 'CONFIG:|TASK-CONFIG:|PROVIDER:|EXECUTION:' kotlin-write.log
Reason about the order rather than memorizing exact unrelated log
lines. Build-script evaluation is configuration. The selected task
must be configured before execution. The Provider transformation is
evaluated when the task action asks for the message. Finally the
task writes the output. If a Provider message appears during
help, something obtained it too early.
8. Engineer an eager pattern, observe the consequence, then repair it
Temporarily add this intentionally poor pattern to the Kotlin build:
// INTENTIONALLY EAGER — diagnostic exercise only.
val eagerAudience = providers.gradleProperty("academyAudience").get()
val eagerTask = tasks.create("eagerMessage") {
doLast { println(eagerAudience) }
}
Now help must obtain academyAudience and
create/configure eagerMessage even though neither is
needed. If you temporarily remove every property source,
help fails because get() throws for the
missing value.
Repair it by returning to:
val audience = providers.gradleProperty("academyAudience")
.orElse("fallback-audience")
val lazyTask = tasks.register("lazyMessage") {
doLast { println(audience.get()) }
}
The fallback is explicit, and the task is merely registered until needed. If the property should be mandatory for only one task, validate it inside that task rather than making the whole build unconfigurable.
9. Optional proof: configuration cache exposes the value/timing boundary
Configuration Cache is not required for the chapter, but it is an excellent diagnostic lens. Use only the disposable lab:
./gradlew -p kotlin-lab writeMessage -PacademyAudience=cache --configuration-cache --console=plain
./gradlew -p kotlin-lab writeMessage -PacademyAudience=cache --configuration-cache --console=plain
On a reusable entry, Gradle can skip configuration; configuration-time messages may disappear while execution still happens or becomes up-to-date according to task inputs/outputs. Do not force Providers during configuration merely to “make the logs consistent.”
10. Challenge: choose the correct build control
A task needs DEPLOY_REGION, but only when
publishPreview executes. Which design is strongest?
Answer: create
val region = providers.environmentVariable("DEPLOY_REGION"), wire that Provider into a task property or obtain/validate it
inside the task’s execution path. Do not call
System.getenv("DEPLOY_REGION") at the top of the build
script and do not print the environment for debugging.
11. Verification checklist and cleanup
- Both Kotlin and Groovy projects ran through the same verified Wrapper.
-
helpdid not requirewriteMessageconfiguration or Provider realization. - Project-property output matched root → user-home → environment → system → CLI precedence tests.
-
The two DSLs produced the same
build/authoring/message.txtbehavior. - The eager pattern was removed after observing its configuration cost/failure coupling.
- No real credential was stored or printed.
./gradlew --stop || true
cd ..
rm -rf gradle-authoring-lab
Only the synthetic lab and its isolated Gradle User Home are
removed. Do not translate this cleanup into deleting your normal
~/.gradle.
Knowledge check
Why use -p kotlin-lab with one Wrapper?
It keeps the Gradle distribution/bootstrap identity constant while selecting a different project directory and DSL build.
With root and user-home academyAudience values,
which one does
providers.gradleProperty() resolve?
The Gradle User Home gradle.properties value has
higher priority than the root build file.
Why does tasks.create() weaken the
help experiment?
It creates/configures the task eagerly during configuration, so an unrelated task pays that cost.
Where should a mandatory property error occur if only one task needs it?
In that task’s relevant configuration/execution path, so unrelated tasks can still configure and run.
Does a Provider transformation necessarily run when the Provider is declared?
No. map constructs another Provider; its
transformation runs when the value is obtained.
What is the evidence that the Groovy port preserved behavior?
Same property resolution, task relationships, phase messages, output location/content, and Wrapper/runtime—not identical punctuation.
12. Bridge to design choices
Now that both DSLs and lazy APIs have been observed rather than merely described, Lesson 3 turns the mechanisms into governance choices: which DSL to standardize, which defaults belong in the repository, when configuration avoidance matters, and when direct scripts should graduate into convention plugins or other build logic.
Official references and version notes
- Gradle 9.7.1 Release Notes — pinned build-engine baseline for this chapter.
- Build Lifecycle — initialization, configuration, execution, and task graph construction.
- Build File Basics and Writing Build Scripts — Groovy/Kotlin DSL scripts and Project model.
- Gradle Kotlin DSL Primer — type-safe model accessors and their timing limitations.
- Properties and Providers and Lazy Configuration.
- Build Environment Configuration — project/system/Gradle/environment property mechanisms and precedence.
-
Task Configuration Avoidance
—
register(),named(),configureEach(), and eager APIs to avoid. - Configuration Cache Requirements — external information sources, environment/system/file/process access, and Provider/ValueSource guidance.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.