gradle
tasks, the task graph, and the kotlin dsl.
- chapter
- chapter 12.06
- reading time
- 7 min read
gradle turns your build into a graph of tasks. you ask for one task, gradle runs only the tasks it depends on, in order, and skips every task whose inputs haven't changed since last time.
the build script is real code (kotlin, usually), it runs in three phases: initialization, configuration, execution. knowing which phase a line runs in is most of the battle.
the picture
maven is an assembly line: fixed stations, always in the same order. gradle is a kitchen with a recipe graph. to serve dinner you need pasta and sauce, the sauce needs chopped onions, the pasta needs boiling water. ask for "sauce" and only the onions and the sauce happen. and if the onions you chopped yesterday are still in the fridge, unchanged, nobody chops them again.
that "still in the fridge" check is the whole trick. every task declares its inputs (source files, classpath, settings) and outputs (class files, a jar). gradle fingerprints both. same inputs, outputs still there: UP-TO-DATE, skipped.
a build script
this is a complete build for a small app with one library and junit. gradle 9.8.1 built it on jdk 25. the kotlin DSL (build.gradle.kts) is what gradle init generates by default now; the older groovy DSL (build.gradle) still works.
plugins {
application // java + a run task + start scripts + zips
}
group = "com.acme"
version = "1.0"
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.7.2-jre")
testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25) // compile + test with a JDK 25
}
}
application {
mainClass = "com.acme.app.App"
}
tasks.test {
useJUnitPlatform()
}it uses the same directory layout as maven (src/main/java, src/test/java) and the same repositories. the difference is what happens when you run it.
the task graph
plugins add tasks and wire them together. with the application plugin, build pulls in 14 tasks. click any task to make it the target, then pretend you edited something.
compileJava reran, but compileTestJava didn't: a method body changed, the class's public API (its ABI) didn't, so nothing compiled against it needs recompiling. tests still rerun.
$ gradle build> Task :compileJava> Task :processResources UP-TO-DATE> Task :classes> Task :jar> Task :startScripts> Task :distTar> Task :distZip> Task :assemble> Task :compileTestJava UP-TO-DATE> Task :processTestResources NO-SOURCE> Task :testClasses UP-TO-DATE> Task :test> Task :check> Task :buildBUILD SUCCESSFUL8 actionable tasks: 6 executed, 2 up-to-date
gradle build --task-graph) and every state is from a real run. the one to try: edit a method body, and watch compileTestJava stay skipped.three phases, and why it matters
a gradle build always runs in the same three phases:
hover a stage.
you can watch it happen with a few printlns:
println("2. build.gradle.kts: configuration")
tasks.register("hello") {
println("3. configuring hello") // configuration phase
doLast {
println("4. running hello") // execution phase
}
}
tasks.register("other") {
println("configuring other") // never printed
}$ gradle hello -q
1. settings.gradle.kts: initialization
2. build.gradle.kts: configuration
3. configuring hello
4. running hello
$ gradle help -q
1. settings.gradle.kts: initialization
2. build.gradle.kts: configurationnotice configuring other never shows up, and hello isn't even configured when you run help. tasks.register is lazy: a task's configuration block only runs if that task ends up in the graph.
dependencies and configurations
gradle's version of maven scopes is the configuration (overloaded word, sorry). the ones you'll use:
| configuration | on the compile classpath | at runtime | maven equivalent |
|---|---|---|---|
implementation | yes | yes | compile, but not leaked to your consumers' compile classpath |
api (java-library plugin) | yes, and your consumers' too | yes | compile |
compileOnly | yes | no | provided |
runtimeOnly | no | yes | runtime |
testImplementation | tests only | tests only | test |
and here's a real difference from maven. same two libraries as the maven chapter, json-lib wanting text-utils 1.0 and http-lib wanting 2.0:
$ gradle dependencies --configuration runtimeClasspath
runtimeClasspath - Runtime classpath of source set 'main'.
+--- com.acme:json-lib:1.0
| \--- com.acme:text-utils:1.0 -> 2.0
\--- com.acme:http-lib:2.0
\--- com.acme:core-lib:2.0
\--- com.acme:text-utils:2.0gradle picked 2.0, the highest version, and the 1.0 -> 2.0 arrow tells you it overrode a request. maven picked 1.0 (nearest) and the app crashed with NoSuchMethodError. neither rule is always right: highest-wins can also break things if 2.0 removed something 1.0 had. but at least you know which rule you're living with.
the speed stack
why gradle builds get faster the more you run them:
- up-to-date checks
- skip a task whose input and output fingerprints match the last run. on by default. the figure above.
- the daemon
- a long-lived background JVM that keeps the build model and a warmed-up JIT between builds. on by default.
- build cache
- reuse a task's outputs from any earlier build with the same inputs, even another branch or another machine (with a remote cache).
--build-cacheororg.gradle.caching=true. - configuration cache
- skip the whole configuration phase when nothing in the build scripts changed. gradle 9 recommends it ("Consider enabling configuration cache to speed up this build", it printed) but still doesn't turn it on for you.
the wrapper, and toolchains+
commit gradlew, gradlew.bat and gradle/wrapper/ to the repo. ./gradlew build downloads exactly the gradle version the project pins, so a fresh clone never needs gradle installed and everyone builds with the same one. toolchains (the java { toolchain { } } block) do the same for the JDK: gradle itself might run on jdk 21 while compiling and testing your code with jdk 25. it finds installed JDKs on its own, and with a toolchain resolver plugin (foojay's is the common one) it can download a missing one.
- gradle runs a graph of tasks, only the ones your target needs.
- every task fingerprints its inputs and outputs. unchanged = UP-TO-DATE, skipped.
- a body-only change skips recompiling the tests: compile avoidance.
- configuration runs every build. real work goes in
doLast. - version conflicts: gradle picks the highest, maven picks the nearest.