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.
Table of Contents
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 abuild.gradlefile is a Groovy closure.dependencies { }passes a closure to thedependencies()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.gradlefiles significantly shorter than equivalentpom.xmlfiles. - 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.
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.
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 agradle.propertiesfile to avoid version duplication across modules - Use
implementationinstead ofcompile(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 inbuildSrc/src/main/groovy/is automatically available in all build scripts - Use the
plugins { }block (notapply plugin:) – it enables Gradle’s plugin portal and version management
DON’T:
- Put expensive logic in configuration phase – code outside
doFirst/doLastblocks runs on every Gradle invocation, evengradle 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 { }ordoFirst { }
Performance Tips
- Enable parallel execution: Add
org.gradle.parallel=truetogradle.propertiesfor multi-project builds. - Enable build caching:
org.gradle.caching=truecaches task outputs and reuses them across builds. - Profile your build: Run
gradle --profile buildto generate a build time report. - Minimize configuration-phase work: Move heavy operations into
doFirst/doLastblocks.
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.gradlefile is a Groovy script – closures, string interpolation, collections, and all Groovy features work inside it - Use
implementationfor dependencies,testImplementationfor test-only dependencies, andcompileOnlyfor annotation processors - Custom tasks use
doFirst { }anddoLast { }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 andproject(':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.
Related Posts
Previous in Series: Groovy Custom Annotations
Next in Series: Groovy Property Files – Configuration Management
Related Topics You Might Like:
- Groovy Grape and @Grab – Dependency Management
- Groovy Closures Tutorial
- Groovy Scripting and Automation
This post is part of the Groovy Cookbook series on TechnoScripts.com

No comment