Gradle Dependency Versions

I’d like to take a moment to describe how to organize Gradle dependency versions in a separate file.  This doesn’t mean this is the only way to do it, of course: it’s just one approach that’s worked well for me.

Step 1: Create a dependencies.gradle file

I usually create this file and add it to the gradle directory.

$ tree --charset=ascii -I 'build|out|src'
.
|-- LICENSE
|-- README.md
|-- build.gradle
|-- gradle
|    |-- dependencies.gradle
|    `-- wrapper
|        |-- gradle-wrapper.jar
|        `-- gradle-wrapper.properties
|-- gradlew
|-- gradlew.bat
`-- settings.gradle

Step 2: Add an ext.versions map containing dependency versions

Add the following code to the dependencies.gradle file:

ext.versions = [
        'assertj': '3.16.1',
        'guava'  : '29.0-jre',
        'jackson': '2.11.2',
        'junit'  : '4.13',
        'logback': '1.2.3',
        'lombok' : '1.18.12',
        'slf4j'  : '1.7.30',
]

Step 3: Apply the file to build.gradle

This imports the versions map variable by applying dependencies.gradle as a script plugin.  This is accomplished by adding the following line to build.gradle:

apply from: 'gradle/dependencies.gradle'

Step 4: Use the versions map in your build file!

Now you can begin using the versions variable in build.gradle.  Instead of hard-coding a version number, reference it from the versions map.  So instead of this:

"org.projectlombok:lombok:1.18.12"

You can use this:

"org.projectlombok:lombok:$versions.lombok"

Here are the full contents of an example build.gradle:

plugins {
    id 'java'
}

apply from: 'gradle/dependencies.gradle'

dependencies {
    runtimeOnly "ch.qos.logback:logback-classic:$versions.logback"
    implementation "com.fasterxml.jackson.core:jackson-databind:$versions.jackson"
    implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$versions.jackson"
    implementation "com.google.guava:guava:$versions.guava"
    compileOnly "org.projectlombok:lombok:$versions.lombok"
    annotationProcessor "org.projectlombok:lombok:$versions.lombok"
    implementation "org.slf4j:slf4j-api:$versions.slf4j"

    testImplementation "junit:junit:$versions.junit"
    testImplementation "org.assertj:assertj-core:$versions.assertj"
    testCompileOnly "org.projectlombok:lombok:$versions.lombok"
    testAnnotationProcessor "org.projectlombok:lombok:$versions.lombok"
}

repositories {
    jcenter()
}

What’s going on here?

Every Gradle build file is backed by an implicit Project instance.  It just so happens that new properties can be added to this Project instance via an extension property named ext.  When we added ext.versions = [...] in dependencies.gradle, we created a Groovy map literal on the Project instance.

A key point here is that Gradle’s build language is based on Groovy.  In build.gradle, when we use the syntax "org.projectlombok:lombok:$versions.lombok", Groovy is smart enough to perform string interpolation on this string literal, replacing $versions.lombok with the version number string 1.18.12 we defined in the map.

Important: for Groovy string interpolation to work correctly, you must use double quotes when defining the dependency coordinates!  This means the following single quoted dependency will result in an error:

compileOnly 'org.projectlombok:lombok:$versions.lombok'

Note: at time of writing I’m using Gradle 6.6 to test this, however the approach should work for older versions as well.