Gradle — Modern Java Build Tool
Learn Gradle — a flexible, fast build tool for Java. Covers build scripts, dependency management, tasks, and the Kotlin DSL.
Gradle is a powerful, flexible build tool that replaces XML with a concise Groovy or Kotlin DSL. Its main advantages over Maven are speed (incremental builds and build caching skip work that hasn’t changed) and expressiveness (you write real code in the build script, not XML). It is the default build system for Android and a popular choice for Spring Boot projects.
Installing Gradle
# macOS
brew install gradle
# Or use the Gradle Wrapper (recommended — no installation needed)
# Most projects include gradlew (Unix) and gradlew.bat (Windows)
# Verify
gradle -v
# Gradle 8.7
The Gradle Wrapper (./gradlew) is the preferred approach — it pins the exact Gradle version your project needs and downloads it automatically. Always use ./gradlew instead of the system gradle so every developer and CI server uses the same version regardless of what they have installed.
Project Structure
Gradle uses the same src/main/java and src/test/java convention as Maven, so the source layout is familiar. The build scripts are in the project root.
my-app/
├── build.gradle.kts ← build script (Kotlin DSL)
├── settings.gradle.kts ← project settings (name, subprojects)
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew ← Unix wrapper script
├── gradlew.bat ← Windows wrapper script
└── src/
├── main/java/ ← same convention as Maven
└── test/java/
settings.gradle.kts
The settings file declares the project name and lists subprojects for multi-project builds. It is always evaluated first — before any build script.
rootProject.name = "my-app"
// For multi-project builds, uncomment:
// include("core", "service", "web")
build.gradle.kts (Kotlin DSL)
This is a complete, real-world build file for a standalone Java application. The java plugin adds compile and test tasks; application adds run and distribution tasks. The toolchain block ensures every developer uses the same JDK version regardless of what they have installed.
plugins {
java
application
}
group = "com.example"
version = "1.0.0"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21)) // consistent JDK across all environments
}
}
application {
mainClass.set("com.example.App")
}
repositories {
mavenCentral() // resolve dependencies from Maven Central
}
dependencies {
// Production dependencies — on the compile and runtime classpath
implementation("com.google.guava:guava:33.1.0-jre")
// Test dependencies — not included in the production output
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("org.mockito:mockito-junit-jupiter:5.11.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform() // required for JUnit 5
}
Groovy DSL (build.gradle) — for reference
The Groovy DSL is more concise but has less IDE support. You will encounter it in older projects.
plugins {
id 'java'
id 'application'
}
group = 'com.example'
version = '1.0.0'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
application {
mainClass = 'com.example.App'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'com.google.guava:guava:33.1.0-jre'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
test {
useJUnitPlatform()
}
Dependency Configurations
Gradle’s dependency configurations are more granular than Maven’s scopes. The most important distinction is implementation vs api — using implementation hides internal dependencies from consumers, which reduces recompilation when you change an implementation detail.
| Configuration | Classpath | Exported to consumers | Use for |
|---|---|---|---|
implementation | compile + runtime | no | normal deps (preferred) |
api | compile + runtime | yes | library deps exposed to consumers |
testImplementation | test compile + runtime | no | JUnit, Mockito |
runtimeOnly | runtime only | no | JDBC drivers |
compileOnly | compile only | no | Lombok, Servlet API |
Use implementation over api — it reduces compilation coupling and speeds up incremental builds by limiting which modules need to recompile when a dependency changes.
Common Gradle Commands
# Build (compile + test + package)
./gradlew build
# Compile only
./gradlew compileJava
# Run tests
./gradlew test
# Run the application
./gradlew run
# Build without tests
./gradlew build -x test
# Clean build outputs
./gradlew clean
# Clean then build — the most reliable way to start fresh
./gradlew clean build
# List all available tasks
./gradlew tasks
# Show dependency tree for the runtime classpath
./gradlew dependencies --configuration runtimeClasspath
# Generate a wrapper for a specific Gradle version
./gradlew wrapper --gradle-version 8.7
Custom Tasks
Custom tasks are one of Gradle’s strengths over Maven — you write real Kotlin or Groovy code rather than XML plugin configuration. Tasks can depend on each other, and Gradle tracks their inputs and outputs for incremental execution.
// Simple custom task — runs as part of any build that calls it
tasks.register("hello") {
description = "Prints a greeting"
group = "custom"
doLast {
println("Hello from Gradle!")
}
}
// Task that depends on another — dependsOn enforces execution order
tasks.register("greetAndBuild") {
dependsOn("hello", "build")
doLast {
println("Build complete!")
}
}
// Typed Copy task — Gradle knows its inputs and outputs for incremental builds
tasks.register<Copy>("copyConfig") {
from("src/main/resources")
into("build/config")
include("*.properties")
}
// Exec task — run any external command as part of the build
tasks.register<Exec>("runDocker") {
commandLine("docker", "run", "-p", "8080:8080", "my-image")
}
Spring Boot with Gradle
Spring Boot’s Gradle plugin adds bootRun, bootJar, and bootBuildImage tasks. The dependency management plugin imports the Spring Boot BOM so you don’t need to specify versions for Spring dependencies — the BOM manages them for you.
plugins {
java
id("org.springframework.boot") version "3.2.5"
id("io.spring.dependency-management") version "1.1.4"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
./gradlew bootRun # run the Spring Boot app in development
./gradlew bootJar # create executable fat JAR
./gradlew bootBuildImage # build a Docker image using Cloud Native Buildpacks
Multi-Project Builds
When a project grows large, splitting it into subprojects lets different teams work independently and lets Gradle build only the modules that changed. The root build script sets up shared configuration that all subprojects inherit.
parent/
├── settings.gradle.kts
├── build.gradle.kts ← shared config applied to all subprojects
├── core/
│ └── build.gradle.kts
└── web/
└── build.gradle.kts
settings.gradle.kts:
rootProject.name = "parent"
include("core", "web") // registers both as subprojects
Root build.gradle.kts — shared config:
subprojects {
apply(plugin = "java")
repositories { mavenCentral() }
// All subprojects share the same test framework version
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
tasks.test { useJUnitPlatform() }
}
web/build.gradle.kts:
dependencies {
// Reference another subproject — Gradle compiles core first
implementation(project(":core"))
}
build.gradle.kts for a Full Spring Boot App
A complete, production-ready Gradle build file combining all the patterns above:
plugins {
java
id("org.springframework.boot") version "3.2.5"
id("io.spring.dependency-management") version "1.1.4"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.mysql:mysql-connector-j")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.mockito:mockito-junit-jupiter:5.11.0")
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed") // show individual test results in the console
}
}