Gradle Core Concepts
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Gradle 9.8.1 (the current release on 2026-10-16; 8.14.6 is the last 8.x release). Every example was run through the
Gradle wrapper on JDK 26, on macOS, in a scratch copy of the tidepool build, with the Kotlin DSL and again with the Groovy DSL. The daemon and the
code both used the same JDK 26; Java toolchains are the subject of a later page. Gradle 8.14 was not available here, so what is said about it comes from
the official documentation and release notes and is labelled "not run here". The build output was trimmed only where a line says so, the scratch
directory was replaced by <work>, and the decimal comma in the program output comes from the locale of the machine.
Gradle is a build tool whose model is a programmable task graph: a build is a set of projects, each project is a set of tasks, and Gradle decides which tasks to run, and in which order, from the dependencies that the build scripts and the plugins declare between them. This page names every concept once, so that the rest of the Gradle pages can use the words without explaining them again. The same concepts in the other two tools are in Ant Build File Structure and Maven POM Fundamentals; a side-by-side comparison of the three models is on Comparing Ant, Maven & Gradle, and the ideas that all three tools share are on Build Automation Fundamentals.
The concepts at a glance
The Gradle user manual introduces six terms on its first page. They are the vocabulary of everything that follows.
| Concept | What it means | Where it lives |
|---|---|---|
Build |
The process and the environment that produce outputs: one invocation of Gradle over one or more projects and their build scripts. |
A directory tree with a settings file at its root (and the |
Project |
A piece of software that can be built, such as an application or a library. A build has one root project and zero or more subprojects. |
One directory per project, named in the settings file. |
Task |
A basic unit of work, such as compiling code, running tests or creating an archive. Tasks are declared in build scripts or added by plugins. |
Registered in a project, addressed by a path such as |
Build script |
A script ( |
The directory of the project it configures. |
Plugin |
A reusable piece of build logic that extends Gradle, usually by adding tasks, configurations and conventions to a project (the Java plugin, for example). |
Applied by a |
Dependency |
Another module or project that a project needs, resolved by Gradle during the build. |
Declared in the |
A seventh item, the settings file, is not in that table but is the one file that makes a directory a build: settings.gradle.kts (or settings.gradle)
names the root project and lists the subprojects. In a single-project build the settings file is optional; in a multi-project build it is mandatory.
The running example
The examples use the three-module tidepool build that the whole section shares: tidepool-model (a library of records), tidepool-engine (a library that
depends on the model and reads a CSV file with Apache Commons CSV) and tidepool-cli (an application that depends on the engine and uses picocli). Because
Gradle builds are scripts, the quickest way to see each concept is to read the few files that define this build.
The project structure
A Gradle project is recognised by the gradlew and gradlew.bat scripts in its root directory. For tidepool the files that Gradle itself reads are these
(the source files of the three modules are omitted):
tidepool
├── gradle
│ ├── libs.versions.toml
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── gradle.properties
├── settings.gradle.kts
├── build.gradle.kts
├── data
│ └── stations.csv
├── tidepool-model
│ ├── build.gradle.kts
│ └── src/main/java/...
├── tidepool-engine
│ ├── build.gradle.kts
│ └── src/main/java/...
└── tidepool-cli
├── build.gradle.kts
└── src/main/java/...
(The tree was written from find output; the ... lines stand for the Java packages.) Running a build also creates two directories next to the settings
file that must not be committed: .gradle/ (project-specific caches, including the configuration cache) and build/ (the outputs of the root project;
every subproject has its own build/ too). What each file is for:
| File | Purpose |
|---|---|
|
The Gradle Wrapper: two scripts, a small JAR and a properties file that name the Gradle version, so that the build downloads and runs the version it was written for. Committed to version control. See Gradle Wrapper; Maven’s counterpart is Maven Wrapper. |
|
The settings file: the name of the build and the list of subprojects. It is evaluated before any build script. |
|
The build script of the root project: configuration shared by all projects, and tasks that belong to the whole build. |
|
One build script per subproject: its plugins and dependencies. |
|
The version catalog: the versions and coordinates of the libraries in one place. Gradle generates type-safe accessors such as |
|
Build settings and project properties, for example |
The settings file
The settings file is a script, the first one Gradle evaluates. It gives the root project a name, declares where dependencies come from for the whole build and includes the subprojects.
rootProject.name = "tidepool"
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
include("tidepool-model", "tidepool-engine", "tidepool-cli")
rootProject.name = 'tidepool'
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
rootProject.name overrides the default, which would be the name of the directory. include creates one project per name, and the name is also the directory
that holds the project. With FAIL_ON_PROJECT_REPOS, a build script that declares its own repositories makes the build fail, which keeps all repository
declarations in this one place. In Maven the same job is split between the parent POM’s <modules> and <repositories>; the Gradle equivalent of a
<module> entry is include. The details of settings files are on the later page
Settings & Build Scripts.
projects shows what Gradle made of this file. With Gradle 9.8.1 and the Kotlin DSL (the Groovy DSL printed the same text):
./gradlew projects
Calculating task graph as no cached configuration is available for tasks: projects
> Configure project :
Configuring :
> Task :projects
Projects:
------------------------------------------------------------
Root project 'tidepool'
------------------------------------------------------------
Location: <work>/tidepool
Project hierarchy:
Root project 'tidepool'
+--- Project ':tidepool-cli'
+--- Project ':tidepool-engine'
\--- Project ':tidepool-model'
Project locations:
project ':tidepool-cli' - /tidepool-cli
project ':tidepool-engine' - /tidepool-engine
project ':tidepool-model' - /tidepool-model
To see a list of the tasks of a project, run gradlew <project-path>:tasks
For example, try running gradlew :tidepool-cli:tasks
BUILD SUCCESSFUL in 5s
1 actionable task: 1 executed
Configuration cache entry stored.
Projects are identified by a path: : is the root project, and :tidepool-cli is the subproject of that name. The first line of the output and the line
Configuring : are explained in the section on the build phases below.
The build scripts
Each subproject has a build script that applies plugins and declares dependencies. The scripts use the version catalog, so no version number appears in them.
[versions]
commons-csv = "1.14.1"
picocli = "4.7.7"
junit = "6.1.3"
[libraries]
commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commons-csv" }
picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
The catalog is a TOML file and is the same for both DSLs; the build scripts of the two other subprojects follow.
plugins {
`java-library`
}
dependencies {
api(project(":tidepool-model"))
implementation(libs.commons.csv)
}
plugins {
id 'java-library'
}
dependencies {
api project(':tidepool-model')
implementation libs.commons.csv
}
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation(libs.picocli)
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation libs.picocli
}
application {
mainClass = 'io.tidepool.cli.Main'
}
The plugins {} block applies the plugins. java-library adds the compile, test and jar tasks and the api and implementation configurations; application
adds run, startScripts and the distribution tasks (distZip, distTar) on top of the Java plugin. A dependency written
as api(project(":tidepool-model")) is a project dependency: it makes Gradle build tidepool-model first and put its JAR on the compile classpath of
tidepool-engine and of everything that depends on tidepool-engine. implementation keeps a library off the compile classpath of consumers (so
tidepool-cli sees Commons CSV only at run time). Dependencies are declared in more detail on
Declaring Dependencies & Configurations and the catalog on
Version Catalogs; Maven’s counterparts are described on
Dependencies, Scopes & Mediation.
The root script has a different job: it holds what is common to the whole build and the tasks that belong to the build itself. It is the next section’s subject.
Properties
gradle.properties supplies build settings. The one in the example turns on the configuration cache, which is why the outputs on this page say
Configuration cache entry stored. and Reusing configuration cache.:
org.gradle.configuration-cache=true
Every org.gradle.* property is covered on
Build Environment & Properties. The configuration cache is off by default in
Gradle 9.8.1 (making it the default is announced for Gradle 10, not released); Gradle 9.0 made it the preferred mode, and Gradle 8.1 made it stable. The
examples on this page are compatible with it, except for the one that is shown failing on purpose.
Tasks and the task graph
A task is the unit of work. Plugins add most tasks, but any build script can register its own. The root script of tidepool registers two tasks and applies
the base plugin, which adds the lifecycle tasks assemble, check, build and clean to a project that has no language plugin.
plugins {
base
}
allprojects {
group = "io.tidepool"
version = "1.0.0"
}
println("Configuring ${project.path}")
val checkStations = tasks.register("checkStations") {
group = "tidepool"
description = "Counts the stations in data/stations.csv."
val stations = layout.projectDirectory.file("data/stations.csv")
doLast {
val count = stations.asFile.readLines().size - 1
println("data/stations.csv lists $count stations")
}
}
tasks.register("release") {
group = "tidepool"
description = "Checks the data and builds the distribution."
dependsOn(checkStations, ":tidepool-cli:distZip")
}
plugins {
id 'base'
}
allprojects {
group = 'io.tidepool'
version = '1.0.0'
}
println "Configuring ${project.path}"
def checkStations = tasks.register('checkStations') {
group = 'tidepool'
description = 'Counts the stations in data/stations.csv.'
def stations = layout.projectDirectory.file('data/stations.csv')
doLast {
def count = stations.asFile.readLines().size() - 1
println "data/stations.csv lists ${count} stations"
}
}
tasks.register('release') {
group = 'tidepool'
description = 'Checks the data and builds the distribution.'
dependsOn checkStations, ':tidepool-cli:distZip'
}
Three ideas are in this script:
-
tasks.registerregisters a task lazily: the block that configures it runs only if the task is part of the requested build (the "task configuration avoidance" API that appeared in Gradle 4.9). The oldertasks.createandtask name { }forms configure the task immediately, even if nobody asked for it. -
doLast { }adds an action, the code that runs in the execution phase. Everything outside it runs while the build is configured. -
dependsOnadds an edge to the graph. The path:tidepool-cli:distZippoints at a task in another project, so the rootreleasetask makes Gradle build the whole distribution of the CLI, and, through the project dependencies, the two libraries before it.
The listing of tasks that a project offers is a task, too:
./gradlew tasks
...
Tidepool tasks
--------------
checkStations - Counts the stations in data/stations.csv.
release - Checks the data and builds the distribution.
...
(Lines marked ... were left out; the full report also lists the build, distribution, help and verification tasks that the base and application
plugins added, and it says that "some of the displayed tasks may belong to subprojects". The group of a task is the heading under which tasks lists it,
and help --task release prints the path, the type, the options and the description of one task.) A task path is built like a project path:
:checkStations is a task of the root project, and :tidepool-cli:run is the task run of the tidepool-cli subproject. Running gradlew run without a
path would run every task called run in every project of the build, which here is the one in tidepool-cli.
The task graph
Gradle does not execute tasks in the order in which they are written. It collects the requested tasks and everything they depend on into a directed acyclic
graph and runs each task after its dependencies. --dry-run prints that order without executing any action:
./gradlew release --dry-run
Calculating task graph as no cached configuration is available for tasks: release
> Configure project :
Configuring :
:checkStations SKIPPED
:tidepool-model:compileJava SKIPPED
:tidepool-engine:compileJava SKIPPED
:tidepool-cli:compileJava SKIPPED
:tidepool-cli:processResources SKIPPED
:tidepool-cli:classes SKIPPED
:tidepool-cli:jar SKIPPED
:tidepool-engine:processResources SKIPPED
:tidepool-engine:classes SKIPPED
:tidepool-engine:jar SKIPPED
:tidepool-model:processResources SKIPPED
:tidepool-model:classes SKIPPED
:tidepool-model:jar SKIPPED
:tidepool-cli:startScripts SKIPPED
:tidepool-cli:distZip SKIPPED
:release SKIPPED
BUILD SUCCESSFUL in 1s
Configuration cache entry stored.
Reading it from the bottom: release needs distZip, which needs startScripts and the jar of the CLI, which needs the compiled classes, which need the
jars of tidepool-engine and tidepool-model, because the CLI depends on the engine and the engine’s api dependency on the model puts the model on the
CLI’s compile classpath too. Nothing connects checkStations to the compilation, so Gradle is free to run them in any order (and with --parallel, at the
same time); the order of independent tasks can differ between runs and between the two DSL runs.
Running the task, with the entry that --dry-run stored in the configuration cache:
./gradlew release
Reusing configuration cache.
> Task :checkStations
data/stations.csv lists 3 stations
> Task :tidepool-engine:processResources NO-SOURCE
> Task :tidepool-cli:processResources NO-SOURCE
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-model:compileJava
> Task :tidepool-model:classes
> Task :tidepool-model:jar
> Task :tidepool-engine:compileJava
> Task :tidepool-engine:classes
> Task :tidepool-engine:jar
> Task :tidepool-cli:compileJava
> Task :tidepool-cli:classes
> Task :tidepool-cli:jar
> Task :tidepool-cli:startScripts
> Task :tidepool-cli:distZip
> Task :release
BUILD SUCCESSFUL in 479ms
9 actionable tasks: 9 executed
Configuration cache entry reused.
The suffix after a task name says what Gradle did with it: no suffix means the task ran, NO-SOURCE means its inputs were empty and it was skipped (there is no
src/main/resources), and the next run shows UP-TO-DATE for tasks whose inputs and outputs did not change:
Reusing configuration cache.
> Task :tidepool-cli:processResources NO-SOURCE
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-engine:processResources NO-SOURCE
> Task :checkStations
data/stations.csv lists 3 stations
> Task :tidepool-model:compileJava UP-TO-DATE
> Task :tidepool-model:classes UP-TO-DATE
> Task :tidepool-model:jar UP-TO-DATE
> Task :tidepool-engine:compileJava UP-TO-DATE
> Task :tidepool-engine:classes UP-TO-DATE
> Task :tidepool-engine:jar UP-TO-DATE
> Task :tidepool-cli:compileJava UP-TO-DATE
> Task :tidepool-cli:classes UP-TO-DATE
> Task :tidepool-cli:jar UP-TO-DATE
> Task :tidepool-cli:startScripts UP-TO-DATE
> Task :tidepool-cli:distZip UP-TO-DATE
> Task :release
BUILD SUCCESSFUL in 561ms
9 actionable tasks: 1 executed, 8 up-to-date
Configuration cache entry reused.
checkStations has no declared inputs or outputs, so Gradle cannot tell that it need not run and always runs it; it is the one executed task of 9 actionable tasks: 1 executed, 8 up-to-date. release and the classes tasks are lifecycle
tasks with no actions of their own: they are not counted among the actionable tasks, and release shows no outcome label at all. Declaring inputs and outputs is the subject of
Incremental Build, the tasks themselves of
Tasks and Writing Custom Tasks.
The Maven counterpart of a task is a plugin goal, which Maven binds to a fixed lifecycle phase
(Maven Build Lifecycle); the Ant counterpart is a target with a depends list
(Ant Targets & Extension Points). The difference is that Gradle’s graph is
built from the dependencies it discovers, including those between the projects, not from a lifecycle that was fixed in advance.
Running the application is also a task. The application plugin’s run task takes its arguments from --args, and the working directory is the project
directory of tidepool-cli, hence the ..:
./gradlew :tidepool-cli:run --args="../data/stations.csv SF" -q
Configuring :
San Francisco: -0,869 m
(-q suppresses the task lines, but not the println of the root script.)
How the concepts fit together
The diagram shows what Gradle does with the files above, from the settings file to the executed tasks.
Gradle names three phases for this flow. In the initialization phase it evaluates the settings file and creates a Project object for every included
project. In the configuration phase it evaluates every build script, which registers tasks and builds the task graph. In the execution phase it runs the
tasks that were requested and what they depend on. A build script’s statements run in the configuration phase unless they are inside an action such as
doLast { }, which is why the println("Configuring ...") of the root script produced output before any task. The phases have their own page,
Build Lifecycle.
The output above shows a consequence. When the configuration cache has no entry for the requested tasks, Gradle prints Calculating task graph as no cached configuration is available and runs the configuration phase (hence Configuring :). When it has one, it prints Reusing configuration cache., skips the
initialization and configuration work and goes straight to execution, so the root script’s println is not run again. That is the reason why an example of
the cache is a good test of the distinction between configuration code and execution code.
A configuration-cache problem
Because the cache stores the task graph and the state of the tasks, the objects a task action refers to must be serialisable. A script-level variable is not. This version of the root script, written the obvious way, keeps the file reference in a variable of the script and uses it in the action:
val stationsFile = layout.projectDirectory.file("data/stations.csv")
val checkStations = tasks.register("checkStations") {
group = "tidepool"
description = "Counts the stations in data/stations.csv."
doLast {
val count = stationsFile.asFile.readLines().size - 1
println("data/stations.csv lists $count stations")
}
}
With org.gradle.configuration-cache=true in gradle.properties, Gradle 9.8.1 runs the tasks and then fails the build when it tries to store the entry
(the same shape in the Groovy DSL, with a def variable, was run too and succeeded, because a def in a Groovy script is a local variable of the script’s run method and is captured on its own; the problem is specific to the Kotlin script’s property):
FAILURE: Build failed with an exception.
* What went wrong:
Configuration cache problems found in this build.
1 problem was found storing the configuration cache.
- Task `:checkStations` of type `org.gradle.api.DefaultTask`: cannot serialize Gradle script object references as these are not supported with the configuration cache.
See https://docs.gradle.org/9.8.1/userguide/configuration_cache_requirements.html#config_cache:requirements:disallowed_types
...
BUILD FAILED in 584ms
9 actionable tasks: 9 executed
Configuration cache entry discarded with 1 problem.
(The lines for the report location and the generic hints were left out.) The fix is the form used in the example above: read the value into a local val inside the
registration block, so that the action captures only the file reference, not the script. The configuration cache is the subject of
Configuration Cache & Isolated Projects.
Invoking Gradle
There are three ways to start a build, and a real project uses the first and the third:
-
In the IDE: IntelliJ IDEA, Android Studio, Visual Studio Code, Eclipse and NetBeans import the build through Gradle’s Tooling API and invoke Gradle when you build, clean or run the application. See CI & IDE Integration.
-
On the command line, with an installed Gradle:
gradle build,gradle test,gradle clean build. See Installation. -
With the wrapper, the recommended way:
./gradlew build(gradlew.bat buildon Windows). The wrapper readsgradle/wrapper/gradle-wrapper.properties, downloads that Gradle version on first use and runs it.
The wrapper of the example project, as it should be committed (the official download URL, the checksum of the -bin distribution and the defaults of the
wrapper task of Gradle 9.8.1; the build was actually run from a local copy of the same archive):
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
The full command line is described on
Command-Line Interface, and the daemon that the client starts on
Gradle Daemon. The single most useful habit is to ask Gradle what it knows, not to read
the scripts: projects, tasks, help --task <task> and dependencies print the structure, the tasks and the dependency tree of a build.
The three tutorial tracks
The official user manual teaches these concepts in three tracks. Each starts with gradle init; the beginner and intermediate tracks generate a Java application
(gradle init --type java-application --dsl kotlin, or --dsl groovy), and the advanced one generates a plugin project:
-
Beginner: six parts, from Initializing the Project and Running Tasks through Understanding Dependencies and Applying Plugins to Exploring Incremental Builds and Enabling the Build Cache. It starts at Part 1: Initializing the Project.
-
Intermediate: seven parts, which cover the same ground as this page and the next ones: Understanding the Build Lifecycle, Multi-Project Builds, Writing the Settings File, Writing a Build Script, Writing Tasks and Writing Plugins. It starts at Part 1: Initializing the Project.
-
Advanced: eight parts that build a plugin step by step: an extension, a custom task, a unit test, a DataFlow action, a functional test, a consumer project and local publishing. It starts at Part 1: Initializing the Project.
gradle init itself is described on Installation. A reasonable reading order for this section is: this
page, the wrapper, the command line, the build lifecycle, the settings file and build scripts, tasks, plugins, then dependencies.
How the concepts differ from Ant and Maven
| Idea | Ant | Maven | Gradle |
|---|---|---|---|
Unit of work |
A task inside a target |
A plugin goal, run in a lifecycle phase |
A task, in a graph |
Project descriptor |
|
|
|
Multi-project |
|
|
|
Extension point |
Custom tasks, macros, antlibs |
Plugins |
Plugins, and tasks written in the build script |
Build logic is |
Imperative XML |
Declarative XML, with the lifecycle fixed |
A script (a program) in Kotlin or Groovy |
Wrapper |
None |
Maven Wrapper, |
Gradle Wrapper, |
What Gradle adds to this table is that the build description is code, so there is more flexibility, and also more room to put work in the wrong phase. Its price is that the build file is not a data format that an IDE or a scanner can read without running Gradle. The pages that follow place the same ideas side by side with the other tools; the first one is Comparing Ant, Maven & Gradle.
Gradle 8.14 and older builds
None of the concepts on this page changed between 8.14 and 9.8.1, and a build of this shape works in both. The differences that matter for the scripts shown here, taken from the release notes and upgrade guides (not run here, because Gradle 8.14 was not available):
-
Gradle 9.0 needs JVM 17 or newer to run, where 8.14 runs on JVM 8 and newer; code can still be compiled for older Java versions through toolchains.
-
The configuration cache became stable in 8.1 and is the preferred mode in 9.0, but it still has to be enabled in both lines; it is announced as the default for Gradle 10.
-
The assignment
mainClass = "..."(with=) in the Kotlin DSL works since Gradle 8.2, where it became the default; older Kotlin scripts usemainClass.set("..."). -
Gradle 9.0 removed
jcenter()and other long-deprecated APIs; a build that uses them needs changes before it runs on 9.x.
Where the books are outdated
The book Gradle in Action (2014) was written for Gradle 1.7. Its model of build, project and task is still right, but several constructs it shows no longer exist in Gradle 9.8.1.
| Book statement or practice | Current status | Source |
|---|---|---|
|
Removed in Gradle 5.0. Use |
|
The configurations |
Removed in Gradle 7.0. Use |
|
|
The |
|
Groovy DSL only |
The Kotlin DSL (stable since 5.0) is the default for |
|
The daemon is opt-in ( |
The daemon is on by default since Gradle 3.0. |
|
Run on JDK 1.5 or newer |
JVM 17 or newer to run Gradle 9.x; older JDKs only through toolchains. |