Skip to main content
Java intermediate Lesson 42 of 58

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.

ConfigurationClasspathExported to consumersUse for
implementationcompile + runtimenonormal deps (preferred)
apicompile + runtimeyeslibrary deps exposed to consumers
testImplementationtest compile + runtimenoJUnit, Mockito
runtimeOnlyruntime onlynoJDBC drivers
compileOnlycompile onlynoLombok, 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
    }
}

Frequently Asked Questions

Should I use Gradle or Maven?
Both are fine for backend Java. Maven is more common in enterprise and has a larger ecosystem of plugins. Gradle is the standard for Android, is faster due to incremental builds and build caching, and its DSL is more expressive. New Spring Boot projects default to Maven; new Android projects use Gradle. Either is a valid choice.
What is the Kotlin DSL vs Groovy DSL?
Gradle supports two script languages. The Groovy DSL (build.gradle) is older and more concise. The Kotlin DSL (build.gradle.kts) provides IDE autocompletion, type safety, and refactoring support. New projects should prefer the Kotlin DSL.
What is Gradle's build cache?
Gradle caches task outputs. If you run the same task twice with the same inputs, Gradle reuses the cached output instead of rerunning — this makes incremental builds very fast. The cache can be shared across machines in a team.