Dependency Management - Best Practices for Naming Gradle Version Catalog Entries

Version catalogs are a fairly recent feature in Gradle Build Tool. They help manage dependencies by providing a standardized way of defining and accessing the catalog of dependencies used in a project—ensuring that all developers in a team are aligned on dependency names and definitions saves time and cognitive load for everyone. Like most Gradle features they are quite flexible, so users have to come up with their own conventions for how to use them.

alt_text

In this blog post, we’re going to share some of the best-practices we employ in the Develocity team when it comes to managing dependencies using version catalogs. In particular, we’re going to look at our convention for how to derive a version catalog entry name from the GAV (short for Group, Artifact ID, Version) coordinates of a particular dependency.

Version catalogs #

Version catalogs are part of Gradle’s dependency management features. They provide a convenient, standardized way to define a set of dependencies that are available to engineers in the project. Version catalogs can either be defined directly in Gradle’s settings script, or using a separate TOML file. The default for a TOML catalog is to place it in gradle/libs.versions.toml. This will create a libs catalog to be used in the build. An example entry in TOML format can look like this:

commons-lang3 = { module = "org.apache.commons:commons-lang3", version = "3.14.0" }

When Gradle finds a version catalog, it generates accessors that can be used in the build scripts to define dependencies in a compile-safe way. So instead of writing:

implementation("org.apache.commons:commons-lang3:3.14.0")

And repeating the GAV coordinate string in each of the build scripts where we need this dependency, we can now write:

implementation(libs.commons.lang3)

As we can see from this example there are some rules that Gradle applies while translating from the catalog entry commons-lang3 to the accessor libs.commons.lang3. Furthermore a question that we could be asking is why I called the entry commons-lang3 instead of e.g. apache-commonsLang3 or org_apache_commons_commons-lang3 (both of which are valid catalog entry names as well).

Catalog entry naming conventions #

Since Gradle does not enforce any naming conventions for entry names other than that they have to be valid TOML entry names, we found ourselves having lots of discussions about why we named a catalog entry one way or the other way. At some point we decided to sit down with the team and write up a list of conventions for how we want to name version catalog entries going forward. Our goal was to stop discussing this on each PR. Moreover we wanted to have consistency in the catalog and give guidance for engineers who need to add new entries. In this section we’re going to share that list of conventions. For each convention, we’ll provide a Group and Artifact ID (GA) tuple and then one or more examples for how not to define the catalog entry, as well as the right way of defining the entry name according to the convention.

Segments in the entry are separated by dashes #

It’s possible to use either dashes or underscores to separate segments in the entry. The recommendation according to the Gradle documentation is to use a dash.

  • GA: org.apache.commons:commons-lang3
  • Wrong: commons_lang3
  • Right: commons-lang3

The first segment should be derived from the project group of the artifact #

Use something that uniquely identifies the project group that produces the artifact. Sometimes the project group matches the Group ID of the GAV. For example in the case of org.slf4j:slf4-api organization and project group are the same – slf4j. In other cases the project group is part of a larger organization, like the commons project group being part of the org.apache organization.

  • GA: org.apache.commons:commons-lang3
  • Wrong: apache-commonsLang, org-apacheCommonsLang
  • Right: commons-lang3

The first segment can be omitted if it would be repeated otherwise #

We like to keep it simple and follow the DRY principle. For that reason if the project group is the same as the artifact, we don’t repeat it.

  • GA: dev.failsafe:failsafe
  • Wrong: dev-failsafe, failsafe-failsafe
  • Right: failsafe

Dashes within the group or artifact ID are translated into camelcase #

When Gradle generates accessors, a dash is translated to a dot, while camelcase is kept as is. This has implications on code completion when inserting a dependency in a build script. For that reason, if the group or artifact contains dashes, we want to keep that part as a unit for code completion. So we replace dashes with camelcase.

  • GA: com.networknt:json-schema-validator
  • Wrong: networknt-json-schema-validator
  • Right: networknt-jsonSchemaValidator

Don’t include terms that are implicit #

Some terms, like java or sdk are implicit when we build JVM code, so they should be omitted. Don’t add things that are obvious for the context of your project’s version catalog.

  • GA: com.amazonaws:aws-java-sdk-core
  • Wrong: aws-javaSdkCore
  • Right: aws-core

Plugin dependencies are always suffixed with -plugin when used as a library #

This is a special case rule for build engineers in our team, because we sometimes build custom plugins on top of other Gradle plugins. In these cases we define implementation dependencies onto the plugin artifacts. To distinguish them from “normal” library dependencies, we add the -plugin suffix. This rule does not apply when they are managed in the [plugins] section of the catalog, which is a dedicated section in the version catalog, that we’re not covering in this blog post.

  • GA: com.bmuschko:gradle-docker-plugin
  • Wrong: bmuschko-gradleDockerPlugin, plugin-bmuschko-docker
  • Right: bmuschko-docker-plugin

Putting it all together #

Now that we’ve discussed our conventions for deriving version catalog entry names from GAVs, let’s apply them for some real-world examples.

Example: SLF4J #

SLF4J ships with an API artifact and several implementations to pick from. We’ll look at the API artifact, which has the org.slf4j4:slf4-api coordinates. Applying conventions for this artifact, we…

  • Identify the project group to be slf4j
  • We drop the org TLD part of the group
  • We don’t want to repeat ourselves, so we strip slf4j from slf4j-api

This gives us slf4j-api as the version catalog entry name for this dependency. Putting this into a libs.versions.toml file would look like this:

[libraries]
slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.13" }

And using this entry in a build script would look like this:

dependencies {
    implementation(libs.slf4j.api)
}

Example: Jackson #

Jackson is a widely used serialization library with lots of components. Let’s look at two examples this time. First we derive an entry name for com.fasterxml.jackson.core:jackson-databind:

  • The project group is jackson
  • We drop all the rest from the group ID
  • We don’t repeat ourselves, so we strip jackson from the artifact name.

The resulting name is jackson-databind.

Now let’s look at another Jackson artifact: com.fasterxml.jackson.dataformat:jackson-dataformat-csv. In this case

  • The project group is again jackson
  • We drop the rest from the group ID
  • We don’t repeat jackson from the artifact
  • We translate the dash in dataformat-csv into camelcase

We end up with jackson-dataformatCsv. As you can see, I made the decision here not to include core or dataformat as a separate segment in the name. Other options would be to use jackson-core-databind and jackson-dataformat-csv, or jacksonCore-databind and jacksonDataformat-csv. I think this is really a matter of taste and how you would like code completion for dependencies to work.

Let’s again put this into a TOML file:

[libraries]
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version = "2.17.1" }
jackson-dataformatCsv = { module = "com.fasterxml.jackson.core:jackson-dataformat-csv", version = "2.17.1" }

And in a build script:

dependencies {
    implementation(libs.jackson.databind)
    implementation(libs.jackson.dataformatCsv)
}

Summary #

Version catalogs are one of Gradle’s dependency management features. They help to centralize dependency GAV coordinates and provide compile-safe access to these coordinates in build scripts. However, Gradle does not enforce any conventions on how to name version catalog entries. In this blog post, we shared some best practices the Develocity engineering team is applying in order to derive version catalog entry names from GAV coordinates. We showed how to apply these conventions using two examples – the SLF4J API as well as two dependencies from the Jackson project.

Discuss