Groovy Gradle Build Automation Guide with 10 Tested Examples

Get started with Groovy Gradle build automation. 10 tested examples covering build.gradle, custom tasks, dependencies, plugins, and multi-project builds.

“A build tool should get out of your way. Gradle, powered by Groovy, does exactly that – it turns build logic into readable, maintainable code.”

Hans Dockter, Gradle Creator

Last Updated: March 2026 | Tested on: Groovy 5.x, Gradle 8.x, Java 17+ | Difficulty: Intermediate | Reading Time: 18 minutes

Every build.gradle file is a Groovy script – the curly braces in dependency blocks are closures, and task declarations are method calls. Understanding how Groovy Gradle integration works – as outlined in the Gradle Groovy Plugin documentation – turns build configuration from a copy-paste exercise into something you actually control, making your entire development workflow smoother.

In this guide, we’ll walk through 10 practical, tested examples that cover everything from setting up a basic build.gradle for a Groovy project, to writing custom tasks, managing dependencies, creating plugins, configuring multi-project builds, testing with Spock, and packaging JARs. Along the way, you’ll see how Gradle’s Groovy DSL works under the hood, and every example is copy-paste-ready for the most common build scenarios.

If you’re new to Groovy closures, take a quick look at our Groovy Closures Tutorial first – closures are the backbone of Gradle’s DSL. For dependency management in scripts (without Gradle), see our Groovy Grape and @Grab guide. And if you’re coming from Maven, stick around for Example 10 where we compare the two.

Why Gradle Uses Groovy

Gradle chose Groovy as its scripting language for a reason. Unlike XML-based build tools (Ant, Maven), Gradle needed a language that could express build logic as real code – conditionals, loops, functions – while still being concise enough for configuration.

  • Closures as configuration blocks – Every { } block in a build.gradle file is a Groovy closure. dependencies { } passes a closure to the dependencies() method.
  • Dynamic typing – Groovy’s dynamic nature lets plugins inject new tasks and configurations at runtime without imports.
  • Java interoperability – Gradle scripts can directly use Java APIs, third-party libraries, and your own compiled classes.
  • Concise syntax – Optional parentheses, property access, and string interpolation make build.gradle files significantly shorter than equivalent pom.xml files.
  • Groovy DSL vs Kotlin DSL – Gradle also supports build.gradle.kts (Kotlin). The Groovy DSL remains more concise and is widely used in Groovy projects and Jenkins pipelines.

How the Gradle Build Lifecycle Works

Every Gradle build goes through three distinct phases: Initialization (reads settings.gradle and identifies projects), Configuration (evaluates build.gradle, creates task objects, and builds the task dependency graph), and Execution (runs the selected tasks in DAG order, executing doFirst and doLast actions). Understanding this lifecycle is key to writing efficient builds.

Common Pitfall

Code OUTSIDE doFirst/doLast
runs in Configuration phase
(every Gradle invocation!)

Phase 3: Execution

Tasks executed
in DAG order

doFirst { }
actions run

doLast { }
actions run

Phase 2: Configuration

build.gradle
evaluated

Task objects
created

Task dependency
graph (DAG) built

Phase 1: Initialization

settings.gradle
evaluated

Projects
identified

Gradle Build Lifecycle – 3 Phases

Setting Up Gradle for Groovy Development

Before the examples, let’s cover the basic setup. You need Java 17+ and Gradle 8.x installed. The easiest way is using the Gradle Wrapper, which most projects include.

Project Initialization

# Create a new Groovy project with Gradle
mkdir my-groovy-project && cd my-groovy-project
gradle init --type groovy-application --dsl groovy

# Or if using the wrapper
./gradlew init --type groovy-application --dsl groovy

# Project structure created:
# my-groovy-project/
# ├── build.gradle
# ├── settings.gradle
# ├── gradlew / gradlew.bat
# ├── gradle/wrapper/
# └── src/
#     ├── main/groovy/
#     └── test/groovy/

Gradle generates a standard project layout. The build.gradle is where all your build configuration lives, settings.gradle defines the project name and subprojects, and the src/ directory follows the Maven convention. Here is what goes inside these files.

10 Practical Examples

Every example below has been tested on Gradle 8.x with Groovy 5.x and Java 17+. We’ll build up from a minimal build.gradle to complex multi-project builds.

Example 1: Basic build.gradle for a Groovy Project

What we’re doing: Creating a minimal but complete build.gradle file that compiles and runs a Groovy application. This is the starting point for every Groovy project.

Example 1: build.gradle – Minimal Groovy Project

// build.gradle - every line here is Groovy code
plugins {
    id 'groovy'           // Adds Groovy compilation support
    id 'application'      // Adds 'run' task for executing the app
}

// Where to download dependencies from
repositories {
    mavenCentral()
}

// Project dependencies
dependencies {
    // Groovy 5.x as the language dependency
    implementation 'org.apache.groovy:groovy:5.0.0-alpha-9'

    // Testing with Spock
    testImplementation 'org.spockframework:spock-core:2.4-M4-groovy-4.0'
    testImplementation 'junit:junit:4.13.2'
}

// Entry point for the 'application' plugin
application {
    mainClass = 'com.example.App'
}

// Java compatibility
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

// Custom project info
println "Building ${project.name} with Groovy ${GroovySystem.version}"

src/main/groovy/com/example/App.groovy

package com.example

class App {
    static void main(String[] args) {
        println "Hello from Groovy ${GroovySystem.version}!"
        println "Running on Java ${System.getProperty('java.version')}"
        println "Built with Gradle"
    }
}

Output (gradle run)

> Task :run
Hello from Groovy 5.0.0-alpha-9!
Running on Java 17.0.2
Built with Gradle

BUILD SUCCESSFUL in 3s

What happened here: The plugins block applies the groovy plugin (which adds tasks like compileGroovy) and the application plugin (which adds a run task). The repositories block tells Gradle to fetch dependencies from Maven Central. The dependencies block declares what libraries your project needs. Every block uses Groovy closures – plugins { } is a method call with a closure argument. The println at the bottom runs during the configuration phase, before any task executes.

Example 2: Adding Dependencies (implementation, testImplementation)

What we’re doing: Understanding Gradle’s dependency configurations – what goes where, why it matters, and how to use version catalogs for cleaner dependency management.

Example 2: Dependency Configurations

plugins {
    id 'groovy'
    id 'application'
}

repositories {
    mavenCentral()
    // Add additional repositories if needed
    maven { url 'https://jitpack.io' }
}

dependencies {
    // --- IMPLEMENTATION ---
    // Available at compile time and runtime, but NOT exposed to consumers
    implementation 'org.apache.groovy:groovy:5.0.0-alpha-9'
    implementation 'org.apache.groovy:groovy-json:5.0.0-alpha-9'
    implementation 'org.apache.groovy:groovy-xml:5.0.0-alpha-9'
    implementation 'com.google.guava:guava:33.0.0-jre'

    // --- API ---
    // Available at compile time and runtime, AND exposed to consumers
    // Use this when your public API returns types from this dependency
    // api 'org.apache.groovy:groovy:5.0.0-alpha-9'  // requires java-library plugin

    // --- COMPILE ONLY ---
    // Available at compile time only, not packaged in the JAR
    compileOnly 'org.projectlombok:lombok:1.18.30'

    // --- RUNTIME ONLY ---
    // Not available at compile time, but included at runtime
    runtimeOnly 'org.slf4j:slf4j-simple:2.0.11'

    // --- TEST IMPLEMENTATION ---
    // Only available in the test source set
    testImplementation 'org.spockframework:spock-core:2.4-M4-groovy-4.0'
    testImplementation 'junit:junit:4.13.2'

    // --- TEST RUNTIME ONLY ---
    testRuntimeOnly 'net.bytebuddy:byte-buddy:1.14.11'
}

// Resolve dependency conflicts with a strategy
configurations.all {
    resolutionStrategy {
        // Force a specific version when there are conflicts
        force 'org.apache.groovy:groovy:5.0.0-alpha-9'

        // Fail on version conflicts instead of silently resolving
        // failOnVersionConflict()

        // Cache dynamic versions for 10 minutes
        cacheDynamicVersionsFor 10, 'minutes'
    }
}

// Print all resolved dependencies
task showDependencies {
    doLast {
        configurations.runtimeClasspath.resolvedConfiguration
            .resolvedArtifacts.each { artifact ->
                println "${artifact.moduleVersion.id} -> ${artifact.file.name}"
            }
    }
}

Output (gradle showDependencies)

> Task :showDependencies
org.apache.groovy:groovy:5.0.0-alpha-9 -> groovy-5.0.0-alpha-9.jar
org.apache.groovy:groovy-json:5.0.0-alpha-9 -> groovy-json-5.0.0-alpha-9.jar
org.apache.groovy:groovy-xml:5.0.0-alpha-9 -> groovy-xml-5.0.0-alpha-9.jar
com.google.guava:guava:33.0.0-jre -> guava-33.0.0-jre.jar
org.slf4j:slf4j-simple:2.0.11 -> slf4j-simple-2.0.11.jar

BUILD SUCCESSFUL in 1s

What happened here: Gradle has several dependency configurations, each controlling when a library is available. implementation is the most common – it makes the library available at compile and runtime. testImplementation restricts availability to tests. compileOnly is for annotation processors and similar tools. The resolutionStrategy block handles version conflicts – a common pain point in JVM projects. The custom showDependencies task uses Gradle’s API to list what actually ends up on the classpath. For quick scripts where you don’t want this setup overhead, see our Groovy Grape and @Grab guide.

Example 3: Custom Gradle Tasks in Groovy

What we’re doing: Writing custom Gradle tasks using Groovy. Since build.gradle is a Groovy script, you have full access to Groovy’s language features inside tasks.

Example 3: Custom Tasks

plugins {
    id 'groovy'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.apache.groovy:groovy:5.0.0-alpha-9'
}

// Simple task with doLast action
task hello {
    description = 'Prints a greeting'
    group = 'Custom'

    doLast {
        println "Hello from Gradle! Today is ${new Date().format('yyyy-MM-dd')}"
    }
}

// Task with doFirst and doLast
task buildInfo {
    description = 'Displays build information'
    group = 'Custom'

    doFirst {
        println "=== Build Info ==="
    }

    doLast {
        println "Project: ${project.name} | Version: ${project.version}"
        println "Gradle: ${gradle.gradleVersion} | Groovy: ${GroovySystem.version}"
        println "JVM: ${System.getProperty('java.version')}"
        println "=== Done ==="
    }
}

// Task using Groovy collections and closures
task analyzeProject {
    description = 'Analyzes project structure'
    group = 'Custom'

    doLast {
        def stats = [:]
        ['groovy', 'java', 'gradle'].each { ext ->
            def files = fileTree('.').matching { include "**/*.${ext}" }.files
            def totalLines = files.collect { it.readLines().size() }.sum() ?: 0
            stats[ext] = [count: files.size(), lines: totalLines]
        }

        println "\nProject Analysis"
        println "=" * 50
        stats.findAll { it.value.count > 0 }.each { ext, data ->
            println String.format("  %-12s %3d files, %5d lines", "*.${ext}", data.count, data.lines)
        }
        println "=" * 50
    }
}

Output (gradle analyzeProject)

> Task :analyzeProject

Project Analysis
==================================================
  *.groovy       4 files,    87 lines
  *.gradle       1 files,    55 lines
==================================================

BUILD SUCCESSFUL in 1s

What happened here: Custom tasks are defined with the task keyword. The doFirst block runs first, doLast runs after. Inside tasks, you have full Groovy – string interpolation, collection methods, file I/O, closures – everything works. The analyzeProject task demonstrates using Groovy’s collect, sum, and each to process file trees – exactly the kind of thing that would be verbose in XML-based build tools. You can also declare outputs.file on tasks to enable Gradle’s incremental build support.

Example 4: Task Dependencies and Ordering

What we’re doing: Controlling the order in which Gradle tasks execute. This is critical for build pipelines where steps must happen in sequence.

How the Task Dependency DAG Works

Gradle builds a Directed Acyclic Graph (DAG) of tasks. When you run a task, Gradle resolves all its dependencies and executes them bottom-up. In this example, running deploy triggers the entire chain: validate -> compile -> test -> build -> deploy, with independent tasks like lint running in parallel when possible.

deploy
(depends on: build)

build
(depends on: test, lint)

test
(depends on: compile)

lint
(independent)

compile
(depends on: validate)

validate
(no dependencies)

Gradle Task DAG – Dependency Execution Order

Example 4: Task Dependencies

plugins {
    id 'groovy'
}

// dependsOn - hard dependency, always runs the prerequisite
task validate {
    group = 'Pipeline'
    doLast { println "[1] Validating project structure... OK" }
}

task compile(dependsOn: validate) {
    group = 'Pipeline'
    doLast { println "[2] Compiling Groovy sources... Done" }
}

task test(dependsOn: compile) {
    group = 'Pipeline'
    doLast { println "[3] Running tests... All passed" }
}

task packageApp(dependsOn: test) {
    group = 'Pipeline'
    doLast { println "[4] Packaging... Output: build/libs/${project.name}.jar" }
}

// mustRunAfter - soft ordering (only applies if both are scheduled)
task lintCheck {
    group = 'Pipeline'
    mustRunAfter validate
    doLast { println "[Lint] Code style check... No issues" }
}

// finalizedBy - always runs after, even on failure
task deploy {
    group = 'Pipeline'
    dependsOn packageApp
    finalizedBy 'deployReport'
    doLast { println "\nDeploying to staging... Success" }
}

task deployReport {
    group = 'Pipeline'
    doLast {
        println "--- Deploy Report ---"
        println "  Time: ${new Date().format('yyyy-MM-dd HH:mm:ss')}"
        println "  Project: ${project.name}"
        println "---------------------"
    }
}

// Run the full pipeline
task fullBuild(dependsOn: deploy) {
    group = 'Pipeline'
    description = 'Runs the full build pipeline'
}

Output (gradle fullBuild)

> Task :validate
[1] Validating project structure... OK
> Task :compile
[2] Compiling Groovy sources... Done
> Task :test
[3] Running tests... All passed
> Task :packageApp
[4] Packaging... Output: build/libs/my-groovy-project.jar
> Task :deploy
Deploying to staging... Success
> Task :deployReport
--- Deploy Report ---
  Time: 2026-03-10 14:30:00
  Project: my-groovy-project
---------------------

BUILD SUCCESSFUL in 2s

What happened here: dependsOn creates a hard dependency – the dependent task always runs first. mustRunAfter is a soft ordering hint – it only takes effect if both tasks are already scheduled to run. finalizedBy ensures a task runs after another, even if the first task fails (useful for cleanup and reporting). Gradle automatically resolves the full chain: validate -> compile -> test -> packageApp -> deploy -> deployReport.

Example 5: Groovy Closures in Gradle DSL

What we’re doing: Exploring how Groovy closures power Gradle’s DSL. This demystifies the “magic” in build.gradle files and shows you how to use closures effectively in your own build logic.

Example 5: Closures in Gradle

plugins {
    id 'groovy'
}

repositories { mavenCentral() }
dependencies { implementation 'org.apache.groovy:groovy:5.0.0-alpha-9' }

// 1. ext block - extra properties via closure
ext {
    appVersion = '2.1.0'
    supportedEnvs = ['dev', 'staging', 'production']
}

// 2. Closure delegates - THIS is how Gradle DSL works
class ServerConfig {
    String host = 'localhost'
    int port = 8080
    boolean ssl = false

    void host(String h) { this.host = h }
    void port(int p) { this.port = p }
    void ssl(boolean s) { this.ssl = s }

    String toString() { "${ssl ? 'https' : 'http'}://${host}:${port}" }
}

def configureServer(Closure cl) {
    def config = new ServerConfig()
    cl.delegate = config
    cl.resolveStrategy = Closure.DELEGATE_FIRST
    cl()
    return config
}

task showServerConfig {
    doLast {
        // This looks like Gradle DSL - because it uses the same pattern!
        def server = configureServer {
            host 'api.example.com'
            port 443
            ssl true
        }
        println "Server: ${server}"
    }
}

// 3. Dynamic task creation with closures
ext.supportedEnvs.each { env ->
    task "deploy${env.capitalize()}" {
        group = 'Deployment'
        doLast {
            println "Deploying v${ext.appVersion} to ${env}..."
            if (env == 'production') println "WARNING: Production deploy!"
        }
    }
}

Output (gradle showServerConfig)

> Task :showServerConfig
Server: https://api.example.com:443

BUILD SUCCESSFUL in 1s

What happened here: The key insight: everything in Gradle is a closure with a delegate. When you write dependencies { implementation '...' }, Gradle sets the closure’s delegate to a DependencyHandler, so implementation resolves to DependencyHandler.implementation(). Our configureServer function uses the same pattern – host 'api.example.com' resolves to ServerConfig.host(). The dynamic task creation loop shows how closures let you generate tasks programmatically – a technique used heavily in real build scripts. For more on closures, see our Groovy Closures Tutorial.

Example 6: Applying and Creating Plugins

What we’re doing: Applying community plugins and creating a custom Gradle plugin written in Groovy, directly inside the build script.

Example 6: Plugins

plugins {
    id 'groovy'
    id 'application'
    id 'jacoco'           // Code coverage (built-in)
    id 'maven-publish'    // Publish to Maven repos (built-in)
}

repositories { mavenCentral() }

dependencies {
    implementation 'org.apache.groovy:groovy:5.0.0-alpha-9'
    testImplementation 'org.spockframework:spock-core:2.4-M4-groovy-4.0'
}

application { mainClass = 'com.example.App' }

// Configuring the JaCoCo plugin
jacoco { toolVersion = '0.8.11' }

jacocoTestReport {
    reports {
        xml.required = true
        html.required = true
    }
}

// --- Custom plugin defined inside build.gradle ---
class BuildInfoPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.extensions.create('buildInfo', BuildInfoExtension)

        project.tasks.register('printBuildInfo') {
            group = 'Reporting'
            description = 'Prints build information'
            doLast {
                def ext = project.extensions.buildInfo
                println "App: ${ext.appName} v${ext.version} [${ext.environment}]"
                println "Gradle: ${project.gradle.gradleVersion} | Groovy: ${GroovySystem.version}"
                println "Java: ${System.getProperty('java.version')}"
            }
        }
    }
}

class BuildInfoExtension {
    String appName = 'MyApp'
    String version = '1.0.0'
    String environment = 'development'
}

// Apply and configure our custom plugin
apply plugin: BuildInfoPlugin

buildInfo {
    appName = 'GroovyGradleDemo'
    version = '2.5.0'
    environment = 'staging'
}

Output (gradle printBuildInfo)

> Task :printBuildInfo
App: GroovyGradleDemo v2.5.0 [staging]
Gradle: 8.6 | Groovy: 5.0.0-alpha-9
Java: 17.0.2

BUILD SUCCESSFUL in 1s

What happened here: We applied built-in plugins (jacoco, maven-publish) and created a custom plugin inside build.gradle. The pattern: implement Plugin<Project>, create an extension class, and register tasks. The buildInfo { } block is our custom DSL – it works like jacoco { } because Gradle uses the same closure-delegate mechanism for all extensions. For larger plugins, move classes to buildSrc/src/main/groovy/.

Example 7: Multi-Project Builds

What we’re doing: Configuring a Gradle multi-project build with shared configuration, cross-project dependencies, and a root build script that orchestrates everything.

Example 7: settings.gradle – Multi-Project

// settings.gradle - defines the project hierarchy
rootProject.name = 'groovy-multi-project'

include 'core'        // Shared utilities and domain objects
include 'api'         // REST API module
include 'cli'         // Command-line interface module

Root build.gradle

// Root build.gradle - shared configuration for all subprojects
subprojects {
    apply plugin: 'groovy'

    repositories { mavenCentral() }

    ext {
        groovyVersion = '5.0.0-alpha-9'
        spockVersion = '2.4-M4-groovy-4.0'
    }

    dependencies {
        implementation "org.apache.groovy:groovy:${groovyVersion}"
        testImplementation "org.spockframework:spock-core:${spockVersion}"
    }

    java {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    test { useJUnitPlatform() }
}

task buildAll {
    dependsOn subprojects.collect { "${it.name}:build" }
    doLast {
        println "\nAll modules built!"
        subprojects.each { println "  - ${it.name}: OK" }
    }
}

core/build.gradle

// core/build.gradle - shared domain objects and utilities
dependencies {
    implementation "org.apache.groovy:groovy-json:${groovyVersion}"
}

// core is a library, not an application
// Other modules depend on this

api/build.gradle

// api/build.gradle - depends on core module
plugins {
    id 'application'
}

dependencies {
    // Depend on sibling module
    implementation project(':core')

    // API-specific dependencies
    implementation 'io.javalin:javalin:6.0.0'
    implementation 'org.slf4j:slf4j-simple:2.0.11'
}

application {
    mainClass = 'com.example.api.ApiServer'
}

cli/build.gradle

// cli/build.gradle - depends on core module
plugins {
    id 'application'
}

dependencies {
    // Depend on sibling module
    implementation project(':core')

    // CLI-specific dependencies
    implementation 'info.picocli:picocli-groovy:4.7.5'
}

application {
    mainClass = 'com.example.cli.CliApp'
}

Output (gradle buildAll)

> Task :core:compileGroovy
> Task :core:jar
> Task :api:compileGroovy
> Task :cli:compileGroovy
> Task :core:build
> Task :api:build
> Task :cli:build
> Task :buildAll

All modules built successfully!
  - core: OK
  - api: OK
  - cli: OK

BUILD SUCCESSFUL in 8s

What happened here: The root build.gradle uses subprojects { } for shared config. Each module’s build.gradle only declares module-specific dependencies. project(':core') creates inter-module dependencies. Gradle resolves build order automatically – core compiles first, then api and cli compile in parallel.

Example 8: Testing Groovy Code with Gradle (Spock)

What we’re doing: Setting up Gradle to run Spock tests for Groovy code, including test configuration, reporting, and filtering.

Example 8: build.gradle – Spock Testing Setup

plugins {
    id 'groovy'
}

repositories { mavenCentral() }

dependencies {
    implementation 'org.apache.groovy:groovy:5.0.0-alpha-9'
    testImplementation 'org.spockframework:spock-core:2.4-M4-groovy-4.0'
    testImplementation platform('org.spockframework:spock-bom:2.4-M4-groovy-4.0')
    testImplementation 'junit:junit:4.13.2'
    testRuntimeOnly 'net.bytebuddy:byte-buddy:1.14.11'  // Mock classes
}

test {
    useJUnitPlatform()  // Required for Spock 2.x

    testLogging {
        events 'passed', 'skipped', 'failed'
        exceptionFormat 'full'
    }

    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1

    // Filter: gradle test --tests '*Calculator*'
    afterSuite { desc, result ->
        if (!desc.parent) {
            println "\nTests: ${result.testCount}, Passed: ${result.successfulTestCount}, Failed: ${result.failedTestCount}"
        }
    }
}

src/main/groovy/com/example/Calculator.groovy

package com.example

class Calculator {
    double add(double a, double b) { a + b }
    double subtract(double a, double b) { a - b }
    double multiply(double a, double b) { a * b }

    double divide(double a, double b) {
        if (b == 0) throw new ArithmeticException("Division by zero")
        a / b
    }

    double power(double base, int exponent) {
        Math.pow(base, exponent)
    }
}

src/test/groovy/com/example/CalculatorSpec.groovy

package com.example

import spock.lang.Specification
import spock.lang.Subject
import spock.lang.Unroll

class CalculatorSpec extends Specification {

    @Subject
    Calculator calc = new Calculator()

    def "should add two numbers"() {
        expect:
        calc.add(2, 3) == 5
        calc.add(-1, 1) == 0
        calc.add(0, 0) == 0
    }

    @Unroll
    def "should calculate #a * #b = #expected"() {
        expect:
        calc.multiply(a, b) == expected

        where:
        a    | b    || expected
        2    | 3    || 6
        -2   | 3    || -6
        0    | 100  || 0
        1.5  | 2    || 3.0
    }

    def "should throw exception on division by zero"() {
        when:
        calc.divide(10, 0)

        then:
        def ex = thrown(ArithmeticException)
        ex.message == "Division by zero"
    }

    @Unroll
    def "should compute #base ^ #exp = #expected"() {
        expect:
        calc.power(base, exp) == expected

        where:
        base | exp || expected
        2    | 3   || 8.0
        3    | 2   || 9.0
        10   | 0   || 1.0
        5    | 1   || 5.0
    }
}

Output (gradle test)

> Task :test

CalculatorSpec > should add two numbers PASSED
CalculatorSpec > should calculate 2 * 3 = 6 PASSED
CalculatorSpec > should calculate -2 * 3 = -6 PASSED
CalculatorSpec > should calculate 0 * 100 = 0 PASSED
CalculatorSpec > should calculate 1.5 * 2 = 3.0 PASSED
CalculatorSpec > should throw exception on division by zero PASSED
CalculatorSpec > should compute 2 ^ 3 = 8.0 PASSED
CalculatorSpec > should compute 3 ^ 2 = 9.0 PASSED
CalculatorSpec > should compute 10 ^ 0 = 1.0 PASSED
CalculatorSpec > should compute 5 ^ 1 = 5.0 PASSED

Tests: 10, Passed: 10, Failed: 0

BUILD SUCCESSFUL in 5s

What happened here: useJUnitPlatform() is required for Spock 2.x. The testLogging block controls console output. The afterSuite closure prints a summary using Gradle’s test listener API. Spock’s @Unroll generates a separate test case for each where row. Reports are generated in build/reports/tests/test/index.html. For more on Spock, check our Groovy Spock Testing guide.

Example 9: Building and Packaging (JAR, Fat JAR)

What we’re doing: Configuring Gradle to produce different types of JAR files – a standard JAR, a fat JAR (uber JAR) with all dependencies bundled, and a distributable archive.

Example 9: Packaging Configuration

plugins {
    id 'groovy'
    id 'application'
}

group = 'com.example'
version = '1.0.0'

repositories { mavenCentral() }

dependencies {
    implementation 'org.apache.groovy:groovy:5.0.0-alpha-9'
    implementation 'org.apache.groovy:groovy-json:5.0.0-alpha-9'
    implementation 'com.google.guava:guava:33.0.0-jre'
}

application { mainClass = 'com.example.App' }

// Standard JAR with manifest
jar {
    manifest {
        attributes(
            'Main-Class': 'com.example.App',
            'Implementation-Title': project.name,
            'Implementation-Version': project.version,
            'Built-Date': new Date().format('yyyy-MM-dd HH:mm:ss')
        )
    }
}

// Fat JAR (Uber JAR) - all dependencies in one JAR
task fatJar(type: Jar) {
    archiveClassifier = 'all'
    description = 'Creates a fat JAR with all dependencies'

    manifest {
        attributes 'Main-Class': 'com.example.App'
    }

    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    with jar
}

// Source JAR
task sourcesJar(type: Jar) {
    archiveClassifier = 'sources'
    from sourceSets.main.allSource
}

// Build all package formats
task packageAll(dependsOn: [jar, fatJar, sourcesJar, 'distZip']) {
    doLast {
        println "\nPackaging Complete!"
        println "  Standard JAR: build/libs/${project.name}-${version}.jar"
        println "  Fat JAR:      build/libs/${project.name}-${version}-all.jar"
        println "  Sources JAR:  build/libs/${project.name}-${version}-sources.jar"
        println "  ZIP Dist:     build/distributions/${project.name}-${version}.zip"
    }
}

Output (gradle packageAll)

> Task :compileGroovy
> Task :jar
> Task :fatJar
> Task :sourcesJar
> Task :distZip
> Task :packageAll

Packaging Complete!
  Standard JAR: build/libs/my-app-1.0.0.jar
  Fat JAR:      build/libs/my-app-1.0.0-all.jar
  Sources JAR:  build/libs/my-app-1.0.0-sources.jar
  ZIP Dist:     build/distributions/my-app-1.0.0.zip

BUILD SUCCESSFUL in 6s

What happened here: The jar block configures the standard JAR with manifest attributes. The fatJar task creates a single JAR containing all dependencies – it uses zipTree() to extract and repackage dependency JARs. The sourcesJar task packages source code (useful for publishing). The distributions block uses the application plugin’s distribution feature to create ZIP and TAR archives with start scripts, JARs, and configuration files. The fat JAR is particularly useful for deploying Groovy applications – just java -jar my-app-1.0.0-all.jar and everything runs without managing classpaths.

Example 10: Gradle vs Maven for Groovy Projects

What we’re doing: Comparing Gradle (Groovy DSL) with Maven (XML) for the same Groovy project setup. This helps you make an informed decision – or understand the codebase you inherited.

Example 10: Gradle build.gradle

// build.gradle - 30 lines for a complete Groovy project setup
plugins {
    id 'groovy'
    id 'application'
    id 'jacoco'
}

group = 'com.example'
version = '1.0.0'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.apache.groovy:groovy:5.0.0-alpha-9'
    implementation 'org.apache.groovy:groovy-json:5.0.0-alpha-9'
    implementation 'com.google.guava:guava:33.0.0-jre'

    testImplementation 'org.spockframework:spock-core:2.4-M4-groovy-4.0'
    testImplementation 'junit:junit:4.13.2'
}

application {
    mainClass = 'com.example.App'
}

test {
    useJUnitPlatform()
}

Equivalent Maven pom.xml (abbreviated)

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-groovy-app</artifactId>
    <version>1.0.0</version>

    <properties>
        <groovy.version>5.0.0-alpha-9</groovy.version>
    </properties>

    <dependencies>
        <!-- Each dependency needs 5 XML lines instead of 1 Gradle line -->
        <dependency>
            <groupId>org.apache.groovy</groupId>
            <artifactId>groovy</artifactId>
            <version>${groovy.version}</version>
        </dependency>
        <!-- ... repeat for groovy-json, guava, spock-core, junit ... -->
    </dependencies>

    <build>
        <plugins>
            <!-- Maven needs gmavenplus-plugin for Groovy compilation -->
            <plugin>
                <groupId>org.codehaus.gmavenplus</groupId>
                <artifactId>gmavenplus-plugin</artifactId>
                <version>3.0.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compileTests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!-- Plus jacoco-maven-plugin, exec-maven-plugin, etc. -->
        </plugins>
    </build>
</project>

Comparison Table

Feature              | Gradle (Groovy DSL)         | Maven (XML)
---------------------|-----------------------------|-------------------------
Config file          | ~30 lines (Groovy code)     | ~80 lines (XML markup)
Groovy compilation   | Built-in (groovy plugin)    | Requires gmavenplus-plugin
Custom logic         | Write Groovy directly       | Write a Maven plugin (Java)
Incremental builds   | Yes (inputs/outputs)        | Limited
Build speed          | Faster (daemon, caching)    | Slower (no daemon)
Multi-project        | Native, flexible            | Module-based, rigid

What happened here: The same project takes ~30 lines in Gradle vs ~80 in Maven. Gradle lets you write custom build logic directly – conditionals, loops, file operations. In Maven, custom logic means writing a Maven plugin in Java. For Groovy projects, Gradle is the natural choice: the groovy plugin handles compilation natively, while Maven requires gmavenplus-plugin. Gradle’s daemon and incremental builds also make it noticeably faster. That said, Maven’s convention-over-configuration approach is perfectly valid if your team already uses it.

Best Practices

Build Script Best Practices

DO:

  • Use the Gradle Wrapper (gradlew) – it ensures everyone uses the same Gradle version, and CI servers don’t need Gradle pre-installed
  • Define dependency versions in one place – use ext { } or a gradle.properties file to avoid version duplication across modules
  • Use implementation instead of compile (deprecated) – it limits dependency leakage and speeds up compilation
  • Declare task inputs and outputs – this enables Gradle’s incremental build and up-to-date checking, dramatically reducing build times
  • Keep build logic in buildSrc/ for complex builds – anything in buildSrc/src/main/groovy/ is automatically available in all build scripts
  • Use the plugins { } block (not apply plugin:) – it enables Gradle’s plugin portal and version management

DON’T:

  • Put expensive logic in configuration phase – code outside doFirst/doLast blocks runs on every Gradle invocation, even gradle tasks
  • Use dynamic dependency versions in production – implementation 'guava:33.+' seems convenient but causes non-reproducible builds
  • Commit the .gradle/ directory – it’s a cache and should be in .gitignore
  • Ignore the Gradle build scan warnings – they often point to real performance issues and deprecated API usage
  • Write imperative task logic at the top level – always put side effects inside doLast { } or doFirst { }

Performance Tips

  • Enable parallel execution: Add org.gradle.parallel=true to gradle.properties for multi-project builds.
  • Enable build caching: org.gradle.caching=true caches task outputs and reuses them across builds.
  • Profile your build: Run gradle --profile build to generate a build time report.
  • Minimize configuration-phase work: Move heavy operations into doFirst/doLast blocks.

Common Pitfalls

Configuration vs Execution Phase: This is the most common mistake in Gradle. Code at the top level of a task block runs during configuration (every time Gradle starts). Only code inside doFirst { } or doLast { } runs during task execution.

Configuration vs Execution Phase

// WRONG - this runs during configuration, even if the task isn't executed
task processData {
    def data = new File('large-dataset.csv').text  // Runs EVERY TIME!
    doLast {
        println "Processing: ${data.size()} bytes"
    }
}

// RIGHT - expensive work is deferred to execution
task processData {
    doLast {
        def data = new File('large-dataset.csv').text  // Runs only when task executes
        println "Processing: ${data.size()} bytes"
    }
}

Groovy DSL vs Kotlin DSL: Both are functionally equivalent. The Groovy DSL is more concise; the Kotlin DSL has better IDE autocompletion. For Groovy projects, the Groovy DSL is the natural fit.

Conclusion

Groovy and Gradle are a natural pair. Every build.gradle file is a Groovy script, and once you see the closures, delegates, and dynamic method resolution, Gradle stops being mysterious and becomes a powerful, programmable build system. From dependency management and custom tasks to multi-project builds and JAR packaging – Groovy’s expressiveness is the thread connecting everything.

Summary

  • Every build.gradle file is a Groovy script – closures, string interpolation, collections, and all Groovy features work inside it
  • Use implementation for dependencies, testImplementation for test-only dependencies, and compileOnly for annotation processors
  • Custom tasks use doFirst { } and doLast { } for execution logic – code outside these runs during configuration
  • Task dependencies (dependsOn), ordering (mustRunAfter), and finalization (finalizedBy) control execution order
  • Gradle’s DSL works through closure delegates – the same pattern you can use in your own Groovy DSL builders
  • Multi-project builds use subprojects { } for shared config and project(':module') for cross-module dependencies
  • Fat JARs bundle all dependencies into a single file for easy deployment
  • For Groovy projects, Gradle is the natural choice over Maven – native Groovy compilation, less XML, and programmable build logic

If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.

Up next: Groovy Property Files – Configuration Management

Frequently Asked Questions

Does Gradle require Groovy to be installed separately?

No. Gradle bundles its own Groovy runtime for executing build.gradle scripts. You don’t need a separate Groovy installation to use Gradle. However, if your project compiles Groovy source code (in src/main/groovy/), you need to declare a Groovy dependency in your build.gradle – such as implementation 'org.apache.groovy:groovy:5.0.0-alpha-9' – so that the Groovy libraries are available at compile time and runtime.

Should I use Groovy DSL or Kotlin DSL for my Gradle build files?

For Groovy projects, the Groovy DSL (build.gradle) is the natural choice – it’s more concise, uses the same language as your source code, and has a larger body of existing examples and documentation. The Kotlin DSL (build.gradle.kts) offers better IDE autocompletion and compile-time checking of build scripts. Both are functionally equivalent and produce identical build behavior. Choose based on your team’s preference and existing codebase.

How do I create a fat JAR (uber JAR) with Gradle for a Groovy project?

Create a custom Jar task that collects all runtime dependencies using configurations.runtimeClasspath.collect { zipTree(it) } and bundles them into a single JAR. Set the Main-Class manifest attribute so the JAR is directly executable with java -jar. Use duplicatesStrategy = DuplicatesStrategy.EXCLUDE to handle duplicate files from different dependencies. Alternatively, use the Shadow plugin (com.github.johnrengelman.shadow) which handles edge cases like service file merging.

What is the difference between dependsOn, mustRunAfter, and finalizedBy in Gradle?

dependsOn creates a hard dependency – if task B dependsOn task A, running B always runs A first. mustRunAfter is a soft ordering hint – it only applies if both tasks are already scheduled to run (it doesn’t cause the other task to run). finalizedBy ensures a task runs after another, even if the first task fails – useful for cleanup tasks and deployment reports. Use dependsOn for required prerequisites, mustRunAfter for ordering optional tasks, and finalizedBy for guaranteed follow-up actions.

Can I use Gradle to build both Groovy and Java code in the same project?

Yes, absolutely. Apply both the groovy and java plugins (though the groovy plugin already extends the java plugin). Put Java files in src/main/java/ and Groovy files in src/main/groovy/. Gradle compiles them together with joint compilation, meaning Groovy classes can reference Java classes and vice versa. This is one of Gradle’s strengths – mixed-language projects work smoothly.

Previous in Series: Groovy Custom Annotations

Next in Series: Groovy Property Files – Configuration Management

Related Topics You Might Like:

This post is part of the Groovy Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *