Central declaration of dependencies

Central declaration of dependencies is an incubating feature. It requires the activation of the VERSION_CATALOGS feature preview.

Using a version catalog

A version catalog is a list of dependencies, represented as dependency coordinates, that a user can pick from when declaring dependencies in a build script.

For example, instead of declaring a dependency using a string notation, the dependency coordinates can be picked from a version catalog:

Example 1. Using a library declared in a version catalog
build.gradle
dependencies {
    implementation(libs.groovy.core)
}
build.gradle.kts
dependencies {
    implementation(libs.groovy.core)
}

In this context, libs is a catalog and groovy represents a dependency available in this catalog. A version catalog provides a number of advantages over declaring the dependencies directly in build scripts:

  • For each catalog, Gradle generates type-safe accessors so that you can easily add dependencies with autocompletion in the IDE.

  • Each catalog is visible to all projects of a build. It is a central place to declare a version of a dependency and to make sure that a change to that version applies to every subproject.

  • Catalogs can declare dependency bundles, which are "groups of dependencies" that are commonly used together.

  • Catalogs can separate the group and name of a dependency from its actual version and use version references instead, making it possible to share a version declaration between multiple dependencies.

Adding a dependency using the libs.someLib notation works exactly like if you had hardcoded the group, artifact and version directly in the build script.

A dependency catalog doesn’t enforce the version of a dependency: like a regular dependency notation, it declares the requested version or a rich version. That version is not necessarily the version that is selected during conflict resolution.

Declaring a version catalog

Version catalogs can be declared in the settings.gradle(.kts) file. In the example above, in order to make groovy available via the libs catalog, we need to associate an alias with GAV (group, artifact, version) coordinates:

Example 2. Declaring a version catalog
settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
        libs {
            alias('groovy-core').to('org.codehaus.groovy:groovy:3.0.5')
            alias('groovy-json').to('org.codehaus.groovy:groovy-json:3.0.5')
            alias('groovy-nio').to('org.codehaus.groovy:groovy-nio:3.0.5')
            alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
                strictly '[3.8, 4.0['
                prefer '3.9'
            }
        }
    }
}
settings.gradle.kts
    dependencyResolutionManagement {
        versionCatalogs {
            create("libs") {
                alias("groovy-core").to("org.codehaus.groovy:groovy:3.0.5")
                alias("groovy-json").to("org.codehaus.groovy:groovy-json:3.0.5")
                alias("groovy-nio").to("org.codehaus.groovy:groovy-nio:3.0.5")
                alias("commons-lang3").to("org.apache.commons", "commons-lang3").version {
                    strictly("[3.8, 4.0[")
                    prefer("3.9")
                }
            }
        }
    }
Mapping of aliases to type-safe accessors

Aliases must consist of a series of identifiers separated by a dash (-, recommended), an underscore (_) or a dot (.). Identifiers themselves must consist of ascii characters, preferably lowercase, eventually followed by numbers.

For example:

  • guava is a valid alias

  • groovy-core is a valid alias

  • commons-lang3 is a valid alias

  • androidx.awesome.lib is also a valid alias

  • but this.#is.not!

Then type safe accessors are generated for each subgroup. For example, given the following aliases:

guava, groovy-core, groovy-xml, groovy-json, androidx.awesome.lib

We would generate the following type-safe accessors:

  • libs.guava

  • libs.groovy.core

  • libs.groovy.xml

  • libs.groovy.json

  • libs.androidx.awesome.lib

In case you want to avoid the generation of a subgroup accessor, we recommend to rely on case to differentiate. For example the aliases groovyCore, groovyJson and groovyXml would be mapped to the libs.groovyCore, libs.groovyJson and libs.groovyXml accessors respectively.

When declaring aliases, it’s worth noting that any of the -, _ and . characters can be used as separators, but the generated catalog will have all normalized to .: for example foo-bar as an alias is converted to foo.bar automatically.

Dependencies with same version numbers

In the example above, we can see that we declare 3 aliases for various components of the groovy library and that all of them share the same version number.

Instead of repeating the same version number, we can declare a version and reference it:

Example 3. Declaring versions separately from libraries
settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
        libs {
            version('groovy', '3.0.5')
            version('checkstyle', '8.37')
            alias('groovy-core').to('org.codehaus.groovy', 'groovy').versionRef('groovy')
            alias('groovy-json').to('org.codehaus.groovy', 'groovy-json').versionRef('groovy')
            alias('groovy-nio').to('org.codehaus.groovy', 'groovy-nio').versionRef('groovy')
            alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
                strictly '[3.8, 4.0['
                prefer '3.9'
            }
        }
    }
}
settings.gradle.kts
    dependencyResolutionManagement {
        versionCatalogs {
            create("libs") {
                version("groovy", "3.0.5")
                version("checkstyle", "8.37")
                alias("groovy-core").to("org.codehaus.groovy", "groovy").versionRef("groovy")
                alias("groovy-json").to("org.codehaus.groovy", "groovy-json").versionRef("groovy")
                alias("groovy-nio").to("org.codehaus.groovy", "groovy-nio").versionRef("groovy")
                alias("commons-lang3").to("org.apache.commons", "commons-lang3").version {
                    strictly("[3.8, 4.0[")
                    prefer("3.9")
                }
            }
        }
    }

Versions declared separately are also available via type-safe accessors, making them usable for more use cases than dependency versions, in particular for tooling:

Example 4. Using a version declared in a version catalog
build.gradle
checkstyle {
    // will use the version declared in the catalog
    toolVersion = libs.versions.checkstyle.get()
}
build.gradle.kts
checkstyle {
    // will use the version declared in the catalog
    toolVersion = libs.versions.checkstyle.get()
}

Dependencies declared in a catalog are exposed to build scripts via an extension corresponding to their name. In the example above, because the catalog declared in settings is named libs, the extension is available via the name libs in all build scripts of the current build. Declaring dependencies using the following notation…​

Example 5. Dependency notation correspondance
build.gradle
dependencies {
    implementation libs.groovy.core
    implementation libs.groovy.json
    implementation libs.groovy.nio
}
build.gradle.kts
dependencies {
    implementation(libs.groovy.core)
    implementation(libs.groovy.json)
    implementation(libs.groovy.nio)
}

…​has exactly the same effect as writing:

Example 6. Dependency notation correspondance
build.gradle
dependencies {
    implementation 'org.codehaus.groovy:groovy:3.0.5'
    implementation 'org.codehaus.groovy:groovy-json:3.0.5'
    implementation 'org.codehaus.groovy:groovy-nio:3.0.5'
}
build.gradle.kts
dependencies {
    implementation("org.codehaus.groovy:groovy:3.0.5")
    implementation("org.codehaus.groovy:groovy-json:3.0.5")
    implementation("org.codehaus.groovy:groovy-nio:3.0.5")
}

Versions declared in the catalog are rich versions. Please refer to the version catalog builder API for the full version declaration support documentation.

Dependency bundles

Because it’s frequent that some dependencies are systematically used together in different projects, a version catalog offers the concept of a "dependency bundle". A bundle is basically an alias for several dependencies. For example, instead of declaring 3 individual dependencies like above, you could write:

Example 7. Using a dependency bundle
build.gradle
dependencies {
    implementation libs.bundles.groovy
}
build.gradle.kts
dependencies {
    implementation(libs.bundles.groovy)
}

The bundle named groovy needs to be declared in the catalog:

Example 8. Declaring a dependency bundle
settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
        libs {
            version('groovy', '3.0.5')
            version('checkstyle', '8.37')
            alias('groovy-core').to('org.codehaus.groovy', 'groovy').versionRef('groovy')
            alias('groovy-json').to('org.codehaus.groovy', 'groovy-json').versionRef('groovy')
            alias('groovy-nio').to('org.codehaus.groovy', 'groovy-nio').versionRef('groovy')
            alias('commons-lang3').to('org.apache.commons', 'commons-lang3').version {
                strictly '[3.8, 4.0['
                prefer '3.9'
            }
            bundle('groovy', ['groovy-core', 'groovy-json', 'groovy-nio'])
        }
    }
}
settings.gradle.kts
    dependencyResolutionManagement {
        versionCatalogs {
            create("libs") {
                version("groovy", "3.0.5")
                version("checkstyle", "8.37")
                alias("groovy-core").to("org.codehaus.groovy", "groovy").versionRef("groovy")
                alias("groovy-json").to("org.codehaus.groovy", "groovy-json").versionRef("groovy")
                alias("groovy-nio").to("org.codehaus.groovy", "groovy-nio").versionRef("groovy")
                alias("commons-lang3").to("org.apache.commons", "commons-lang3").version {
                    strictly("[3.8, 4.0[")
                    prefer("3.9")
                }
                bundle("groovy", listOf("groovy-core", "groovy-json", "groovy-nio"))
            }
        }
    }

The semantics are again equivalent: adding a single bundle is equivalent to adding all dependencies which are part of the bundle individually.

Plugins

In addition to libraries, version catalog supports declaring plugin versions. While libraries are represented by their group, artifact and version coordinates, Gradle plugins are identified by their id and version only. Therefore, they need to be declared separately:

Example 9. Declaring a plugin version
settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
        libs {
            alias('jmh').toPluginId('me.champeau.jmh').version('0.6.5')
        }
    }
}
settings.gradle.kts
    dependencyResolutionManagement {
        versionCatalogs {
            create("libs") {
                alias("jmh").toPluginId("me.champeau.jmh").version("0.6.5")
            }
        }
    }

Then the plugin is accessible in the plugins block and can be consumed in any project of the build using:

Example 10. Using a plugin declared in a catalog
build.gradle
plugins {
    id 'java-library'
    id 'checkstyle'
    // Use the plugin `jmh` as declared in the `libs` version catalog
    alias(libs.plugins.jmh)
}
build.gradle.kts
plugins {
    `java-library`
    checkstyle
    alias(libs.plugins.jmh)
}

The libs.versions.toml file

In addition to the settings API above, Gradle offers a conventional file to declare a catalog. If a libs.versions.toml file is found in the gradle subdirectory of the root build, then a catalog will be automatically declared with the contents of this file.

Declaring a libs.versions.toml file doesn’t make it the single source of truth for dependencies: it’s a conventional location where dependencies can be declared. As soon as you start using catalogs, it’s strongly recommended to declare all your dependencies in a catalog and not hardcode group/artifact/version strings in build scripts. Be aware that it may happen that plugins add dependencies, which are dependencies defined outside of this file.

Just like src/main/java is a convention to find the Java sources, which doesn’t prevent additional source directories to be declared (either in a build script or a plugin), the presence of the libs.versions.toml file doesn’t prevent the declaration of dependencies elsewhere.

The presence of this file does, however, suggest that most dependencies, if not all, will be declared in this file. Therefore, updating a dependency version, for most users, should only consists of changing a line in this file.

By default, the libs.versions.toml file will be an input to the libs catalog. It is possible to change the name of the default catalog, for example if you already have an extension with the same name:

Example 11. Changing the default extension name
settings.gradle
dependencyResolutionManagement {
    defaultLibrariesExtensionName.set('deps')
}
settings.gradle.kts
dependencyResolutionManagement {
    defaultLibrariesExtensionName.set("deps")
}

The version catalog TOML file format

The TOML file consists of 4 major sections:

  • the [versions] section is used to declare versions which can be referenced by dependencies

  • the [libraries] section is used to declare the aliases to coordinates

  • the [bundles] section is used to declare dependency bundles

  • the [plugins] section is used to declare plugins

For example:

The libs.versions.toml file
[versions]
groovy = "3.0.5"
checkstyle = "8.37"

[libraries]
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" }
groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer="3.9" } }

[bundles]
groovy = ["groovy-core", "groovy-json", "groovy-nio"]

[plugins]
jmh = { id = "me.champeau.jmh", version = "0.6.5" }

Versions can be declared either as a single string, in which case they are interpreted as a required version, or as a rich versions:

[versions]
my-lib = { strictly = "[1.0, 2.0[", prefer = "1.2" }

Supported members of a version declaration are:

Dependency declaration can either be declared as a simple string, in which case they are interpreted as group:artifact:version coordinates, or separating the version declaration from the group and name:

Different dependency notations
[versions]
common = "1.4"

[libraries]
my-lib = "com.mycompany:mylib:1.4"
my-other-lib = { module = "com.mycompany:other", version = "1.4" }
my-other-lib2 = { group = "com.mycompany", name = "alternate", version = "1.4" }
mylib-full-format = { group = "com.mycompany", name = "alternate", version = { require = "1.4" } }

[plugins]
short-notation = "some.plugin.id:1.4"
long-notation = { id = "some.plugin.id", version = "1.4" }
reference-notation = { id = "some.plugin.id", version.ref = "common" }

In case you want to reference a version declared in the [versions] section, you should use the version.ref property:

[versions]
some = "1.4"

[libraries]
my-lib = { group = "com.mycompany", name="mylib", version.ref="some" }

The TOML file format is very lenient and lets you write "dotted" properties as shortcuts to full object declarations. For example, this:

a.b.c="d"

is equivalent to:

a.b = { c = "d" }

or

a = { b = { c = "d" } }

See the TOML specification for details.

Sharing catalogs

Version catalogs are used in a single build (possibly multi-project build) but may also be shared between builds. For example, an organization may want to create a catalog of dependencies that different projects, from different teams, may use.

Importing a catalog from a TOML file

The version catalog builder API supports including a model from an external file. This makes it possible to reuse the catalog of the main build for buildSrc, if needed. For example, the buildSrc/settings.gradle(.kts) file can include this file using:

Example 12. Sharing the dependency catalog with buildSrc
settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
        libs {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}
settings.gradle.kts
dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

This technique can therefore be used to declare multiple catalogs from different files:

Example 13. Declaring additional catalogs
settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
        // declares an additional catalog, named 'testLibs', from the 'test-libs.versions.toml' file
        testLibs {
            from(files('gradle/test-libs.versions.toml'))
        }
    }
}
settings.gradle.kts
dependencyResolutionManagement {
    versionCatalogs {
        // declares an additional catalog, named 'testLibs', from the 'test-libs.versions.toml' file
        create("testLibs") {
            from(files("gradle/test-libs.versions.toml"))
        }
    }
}

The version catalog plugin

While importing catalogs from local files is convenient, it doesn’t solve the problem of sharing a catalog in an organization or for external consumers. One option to share a catalog is to write a settings plugin, publish it on the Gradle plugin portal or an internal repository, and let the consumers apply the plugin on their settings file.

Alternatively, Gradle offers a version catalog plugin, which offers the ability to declare, then publish a catalog.

To do this, you need to apply the version-catalog plugin:

Example 14. Applying the version catalog plugin
build.gradle
plugins {
    id 'version-catalog'
    id 'maven-publish'
}
build.gradle.kts
plugins {
    `version-catalog`
    `maven-publish`
}

This plugin will then expose the catalog extension that you can use to declare a catalog:

Example 15. Definition of a catalog
build.gradle
catalog {
    // declare the aliases, bundles and versions in this block
    versionCatalog {
        alias('my-lib').to('com.mycompany:mylib:1.2')
    }
}
build.gradle.kts
catalog {
    // declare the aliases, bundles and versions in this block
    versionCatalog {
        alias("my-lib").to("com.mycompany:mylib:1.2")
    }
}

Such a catalog can then be published by applying either the maven-publish or ivy-publish plugin and configuring the publication to use the versionCatalog component:

Example 16. Publishing a catalog
build.gradle
publishing {
    publications {
        maven(MavenPublication) {
            from components.versionCatalog
        }
    }
}
build.gradle.kts
publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["versionCatalog"])
        }
    }
}

When publishing such a project, a libs.versions.toml file will automatically be generated (and uploaded), which can then be consumed from other Gradle builds.

Importing a published catalog

A catalog produced by the version catalog plugin can be imported via the settings API:

Example 17. Using a published catalog
settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
        libs {
            from("com.mycompany:catalog:1.0")
        }
    }
}
settings.gradle.kts
    dependencyResolutionManagement {
        versionCatalogs {
            create("libs") {
                from("com.mycompany:catalog:1.0")
            }
        }
    }

Overwriting catalog versions

In case a catalog declares a version, you can overwrite the version when importing the catalog:

Example 18. Overwriting versions declared in a published catalog
settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
        amendedLibs {
            from("com.mycompany:catalog:1.0")
            // overwrite the "groovy" version declared in the imported catalog
            version("groovy", "3.0.6")
        }
    }
}
settings.gradle.kts
    dependencyResolutionManagement {
        versionCatalogs {
            create("amendedLibs") {
                from("com.mycompany:catalog:1.0")
                // overwrite the "groovy" version declared in the imported catalog
                version("groovy", "3.0.6")
            }
        }
    }

In the example above, any dependency which was using the groovy version as reference will be automatically updated to use 3.0.6.

Again, overwriting a version doesn’t mean that the actual resolved dependency version will be the same: this only changes what is imported, that is to say what is used when declaring a dependency. The actual version will be subject to traditional conflict resolution, if any.

Using a platform to control transitive versions

A platform is a special software component which can be used to control transitive dependency versions. In most cases it’s exclusively composed of dependency constraints which will either suggest dependency versions or enforce some versions. As such, this is a perfect tool whenever you need to share dependency versions between projects. In this case, a project will typically be organized this way:

  • a platform project which defines constraints for the various dependencies found in the different sub-projects

  • a number of sub-projects which depend on the platform and declare dependencies without version

In the Java ecosystem, Gradle provides a plugin for this purpose.

It’s also common to find platforms published as Maven BOMs which Gradle supports natively.

A dependency on a platform is created using the platform keyword:

Example 19. Getting versions declared in a platform
build.gradle
dependencies {
    // get recommended versions from the platform project
    api platform(project(':platform'))
    // no version required
    api 'commons-httpclient:commons-httpclient'
}
build.gradle.kts
dependencies {
    // get recommended versions from the platform project
    api(platform(project(":platform")))
    // no version required
    api("commons-httpclient:commons-httpclient")
}

This platform notation is a short-hand notation which actually performs several operations under the hood:

  • it sets the org.gradle.category attribute to platform, which means that Gradle will select the platform component of the dependency.

  • it sets the endorseStrictVersions behavior by default, meaning that if the platform declares strict dependencies, they will be enforced.

This means that by default, a dependency to a platform triggers the inheritance of all strict versions defined in that platform, which can be useful for platform authors to make sure that all consumers respect their decisions in terms of versions of dependencies. This can be turned off by explicitly calling the doNotEndorseStrictVersions method.

Importing Maven BOMs

Gradle provides support for importing bill of materials (BOM) files, which are effectively .pom files that use <dependencyManagement> to control the dependency versions of direct and transitive dependencies. The BOM support in Gradle works similar to using <scope>import</scope> when depending on a BOM in Maven. In Gradle however, it is done via a regular dependency declaration on the BOM:

Example 20. Depending on a BOM to import its dependency constraints
build.gradle
dependencies {
    // import a BOM
    implementation platform('org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE')

    // define dependencies without versions
    implementation 'com.google.code.gson:gson'
    implementation 'dom4j:dom4j'
}
build.gradle.kts
dependencies {
    // import a BOM
    implementation(platform("org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE"))

    // define dependencies without versions
    implementation("com.google.code.gson:gson")
    implementation("dom4j:dom4j")
}

In the example, the versions of gson and dom4j are provided by the Spring Boot BOM. This way, if you are developing for a platform like Spring Boot, you do not have to declare any versions yourself but can rely on the versions the platform provides.

Gradle treats all entries in the <dependencyManagement> block of a BOM similar to Gradle’s dependency constraints. This means that any version defined in the <dependencyManagement> block can impact the dependency resolution result. In order to qualify as a BOM, a .pom file needs to have <packaging>pom</packaging> set.

However often BOMs are not only providing versions as recommendations, but also a way to override any other version found in the graph. You can enable this behavior by using the enforcedPlatform keyword, instead of platform, when importing the BOM:

Example 21. Importing a BOM, making sure the versions it defines override any other version found
build.gradle
dependencies {
    // import a BOM. The versions used in this file will override any other version found in the graph
    implementation enforcedPlatform('org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE')

    // define dependencies without versions
    implementation 'com.google.code.gson:gson'
    implementation 'dom4j:dom4j'

    // this version will be overridden by the one found in the BOM
    implementation 'org.codehaus.groovy:groovy:1.8.6'
}
build.gradle.kts
dependencies {
    // import a BOM. The versions used in this file will override any other version found in the graph
    implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE"))

    // define dependencies without versions
    implementation("com.google.code.gson:gson")
    implementation("dom4j:dom4j")

    // this version will be overridden by the one found in the BOM
    implementation("org.codehaus.groovy:groovy:1.8.6")
}

Should I use a platform or a catalog?

Because platforms and catalogs both talk about dependency versions and can both be used to share dependency versions in a project, there might be a confusion regarding what to use and if one is preferable to the other.

The answer is that you should always use catalogs and maybe use platforms in addition.

A catalog helps with centralizing the dependency versions and is only, as it name implies, a catalog of dependencies you can pick from. We recommend using it to declare the coordinates of your dependencies, in all cases. It will be used by Gradle to generate type-safe accessors, present short-hand notations for external dependencies and it allows sharing those coordinates between different projects easily. Using a catalog will not have any kind of consequence on downstream consumers: it’s transparent to them.

A platform is a more heavyweight construct: it’s a component of a dependency graph, like any other library. If you depend on a platform, that platform is itself a component in the graph. It means, in particular, that:

  • Constraints defined in a platform can influence transitive dependencies, not only the direct dependencies of your project.

  • A platform is versioned, and a transitive dependency in the graph can depend on a different version of the platform, causing various dependency upgrades.

  • A platform can tie components together, and in particular can be used as a construct for aligning versions.

  • A dependency on a platform is "inherited" by the consumers of your dependency: it means that a dependency on a platform can influence what versions of libraries would be used by your consumers even if you don’t directly, or transitively, depend on components the platform references.

In summary, using a catalog is always a good engineering practice as it centralizes common definitions, allows sharing of dependency versions or plugin versions, but it is an "implementation detail" of the build: it will not be visible to consumers and unused elements of a catalog are just ignored.

A platform is meant to influence the dependency resolution graph, for example by adding constraints on transitive dependencies: it’s a solution for structuring a dependency graph and influencing the resolution result.

In practice, your project can both use a catalog and declare a platform which itself uses the catalog:

Example 22. Using a catalog within a platform definition
build.gradle
plugins {
    id 'java-platform'
}

dependencies {
    constraints {
        api(libs.mylib)
    }
}
build.gradle.kts
plugins {
    `java-platform`
}

dependencies {
    constraints {
        api(libs.mylib)
    }
}