Java Gradle Build Script
Anatomy of a Gradle build script for Java — plugins, dependencies, tasks, and DSL basics.
A Gradle build script describes how to compile, test, and package a project — not as a rigid XML document, but as code. Gradle reads a build.gradle file (Groovy DSL) or build.gradle.kts file (Kotlin DSL), turns the declarations inside into a graph of tasks, and runs only the tasks your command needs, in the right order. Where Maven gives you a fixed lifecycle, Gradle gives you a programmable one: applying a plugin, declaring a dependency, and defining a task are all ordinary statements in a real language.
This page assumes you already know what Gradle is at a high level — if not, start with the Gradle introduction. Here we take apart a real build.gradle block by block: plugins, repositories, dependencies, and tasks.
The anatomy of build.gradle
A minimal Java build script has four blocks: which plugins to apply, where to fetch dependencies, what those dependencies are, and a little configuration. Here is a complete, idiomatic one in the Kotlin DSL:
plugins {
java
application
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:32.1.3-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
application {
mainClass = "com.example.App"
}
tasks.test {
useJUnitPlatform()
}The java plugin alone gives you compileJava, test, jar, and a dozen more tasks for free. The application plugin adds run and installDist. The tasks.test { useJUnitPlatform() } line wires the test task to run JUnit 5 tests — without it Gradle defaults to the legacy JUnit 4 engine and silently runs nothing. Everything after that is configuration of what those tasks do.
Plugins, repositories, and the build lifecycle
Almost nothing in Gradle is built in — capabilities arrive as plugins. The java plugin is the foundation for JVM work; others layer on top:
| Plugin | What it adds |
|---|---|
java | compileJava, test, jar, source sets, the dependencies configurations |
application | run and a packaged distribution with start scripts |
java-library | The api vs implementation distinction for libraries |
org.springframework.boot | bootJar, bootRun for Spring Boot apps |
jacoco | Code-coverage reporting wired into test |
repositories { } tells Gradle where to download dependencies from — mavenCentral() is the usual choice. Without a repository, no external dependency can be resolved.
Declaring dependencies and their scopes
Dependencies are declared with a configuration that controls where they appear on the classpath. Picking the right one keeps your compile classpath clean and your builds fast:
dependencies {
// On the compile and runtime classpath, but NOT exposed to consumers
implementation("org.apache.commons:commons-lang3:3.14.0")
// Part of this library's public API — leaks to consumers (java-library only)
api("com.google.guava:guava:32.1.3-jre")
// Needed to compile, but provided at runtime by the environment
compileOnly("org.projectlombok:lombok:1.18.30")
// Only on the test classpath
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
// Only at runtime (e.g. a JDBC driver loaded by name)
runtimeOnly("org.postgresql:postgresql:42.7.1")
}The coordinate format is group:name:version — the same coordinates Maven uses in its pom.xml. When two dependencies pull in the same module at different versions, Gradle's default strategy puts a single, highest version on the classpath — the runnable example below models exactly this.
Tasks: the unit of work
Every action Gradle performs is a task, and tasks declare dependencies on other tasks. Running gradle build does not run one thing; it walks a graph and executes each prerequisite once. You can also define your own:
tasks.register("printVersion") {
group = "help"
description = "Prints the project version."
doLast {
println("Project version is $version")
}
}
// Make the jar task wait for our custom task
tasks.named("jar") {
dependsOn("printVersion")
}Two more facts make Gradle fast. First, it is incremental: a task whose inputs and outputs are unchanged is reported UP-TO-DATE and skipped. Second, the Gradle Wrapper (./gradlew, backed by gradle/wrapper/gradle-wrapper.properties) pins one Gradle version per project, so every developer and CI machine builds with the same toolchain — you never install Gradle globally.
A worked example: a build, modelled in plain Java
Gradle itself is not on this runner, so the program below models the three ideas that make a build script work — the task graph and its execution order, incremental up-to-date skipping, and dependency version conflict resolution — using nothing but the JDK. It is the mental model gradle build executes for real.
What to take from the run:
- The task list for
gradle buildis computed by a topological sort, not written out by hand.compileJavaandprocessResourcescome beforeclasses, which comes beforejarandtest, which come beforebuild— exactly the order Gradle prints with each:taskNameprefix, because a task can only run after everything itdependsOnhas finished. - A diamond in the graph runs a shared prerequisite once, not twice. Both
jarandtestdepend onclasses, yetclassesappears a single time in the order — thedoneset is what stops Gradle from recompiling the same code for every downstream task. - The second run shows Gradle's incremental behaviour:
compileJava,processResources, andclassesareUP-TO-DATEand skipped, so only3tasks actually execute. This is why an unchanged project rebuilds in milliseconds — Gradle compares task inputs and outputs and does no work it can avoid. - Dependency resolution collapses a version conflict to one winner:
slf4j-apiis requested at both2.0.9(direct) and1.7.36(transitive via guava), but the resolved classpath lists it once at2.0.9. Gradle's default strategy is highest-version-wins, so a single, consistent jar lands on the classpath instead of two clashing copies. - The final line names the Gradle version
8.7as if read fromgradle-wrapper.properties. In a real project the wrapper stores that version in source control, so./gradlew builduses the same Gradle for everyone — the build is reproducible regardless of what (if anything) is installed on the machine.