# Best Practices for Dependencies

version 9.0.0

## Use Version Catalogs to Centralize Dependency Versions

Use Version Catalogs provide a centralized, declarative way to manage dependency versions throughout a build.

### Explanation

When you define your dependency versions in a single, shared version catalog, you reduce duplication and make upgrades easier. Instead of changing dozens of `build.gradle(.kts)` files, you update the version in one place. This simplifies maintenance, improves consistency, and reduces the risk of accidental version drift between modules. Consistent version declarations across projects also make it easier to reason about behavior during testing—especially in modular builds where transitive upgrades can silently change runtime behavior in later stages of the build.

However, version catalogs only influence declared versions, not resolved versions. Use them in combination with dependency locking and version alignment to enforce consistency across builds. To influence resolved versions, check out platforms.

### Example

#### Don’t Do This

Avoid declaring versions in `project.ext`, constants, or local variables:

build.gradle.kts

    plugins {
        id("java-library")
        id("com.github.ben-manes.versions").version("0.45.0")
    }
    val groovyVersion = "3.0.5"

    dependencies {
        api("org.codehaus.groovy:groovy:$groovyVersion")
        api("org.codehaus.groovy:groovy-json:$groovyVersion")
        api("org.codehaus.groovy:groovy-nio:$groovyVersion")

        testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    }

    dependencies {
        implementation("org.apache.commons:commons-lang3") {
            version {
                strictly("[3.8, 4.0[")
                prefer("3.9")
            }
        }
    }

build.gradle

    plugins {
        id('java-library')
        id('com.github.ben-manes.versions').version('0.45.0')
    }
    def groovyVersion = '3.0.5'

    dependencies {
        api("org.codehaus.groovy:groovy:$groovyVersion")
        api("org.codehaus.groovy:groovy-json:$groovyVersion")
        api("org.codehaus.groovy:groovy-nio:$groovyVersion")

        testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")

        implementation("org.apache.commons:commons-lang3") {
            version {
                strictly("[3.8, 4.0[")
                prefer("3.9")
            }
        }
    }

Avoid misusing version catalogs for unrelated concerns:

- Don’t use them to store shared strings or non-library constants

- Don’t overload them with arbitrary logic or plugin-specific configuration

#### Do This Instead

Use a centralized `libs.versions.toml` file in your `gradle/` directory:

gradle/libs.versions.toml

    [versions]
    groovy = "3.0.5"
    junit-jupiter = "5.10.0"

    [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" } }
    junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }

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

    [plugins]
    versions = { id = "com.github.ben-manes.versions", version = "0.45.0" }

build.gradle.kts

    plugins {
        id("java-library")
        alias(libs.plugins.versions)
    }
    dependencies {
        api(libs.bundles.groovy)
        testImplementation(libs.junit.jupiter)
        implementation(libs.commons.lang3)
    }

build.gradle

    plugins {
        id('java-library')
        alias(libs.plugins.versions)
    }
    dependencies {
        api(libs.bundles.groovy)
        testImplementation(libs.junit.jupiter)
        implementation(libs.commons.lang3)
    }

### References

- Version Catalogs

### Tags

`#version-catalog`

## Name Version Catalog Entries Appropriately

Consistent and descriptive names in your version catalog enhance readability and maintainability across your build scripts.

### Explanation

Version catalogs provide a centralized way to manage dependencies. Adopting clear naming conventions for catalog entries ensures that developers can easily identify and use dependencies throughout the project.

The following guidelines help in naming catalog entries effectively:

1. Use dashes to separate segments: Prefer dashes (`-`) over underscores (`_`) to separate different parts of the entry name.

   Example: For `org.apache.logging.log4j:log4j-api`, use `log4j-api`

2. Derive the first segment from the project group: Use a unique identifier from the project’s group ID as the first segment.

   Example: For `com.fasterxml.jackson.core:jackson-databind`, use `jackson-databind`

3. Omit redundant segments: If the group and artifact IDs are the same, avoid repeating them.

   Example: For `io.ktor:ktor-client-core`, use `ktor-client-core`, not `ktor-ktor-client-core`

4. Convert internal dashes to camelCase: If the artifact ID contains dashes, convert them to camelCase for better readability in code.

   Example: `spring-boot-starter-web` becomes `springBootStarterWeb`

5. Avoid implicit terms: Exclude terms that are obvious or implied in the context of your project.

   Example: For `com.amazonaws:aws-java-sdk-core`, use `aws-core`, not `aws-javaSdkCore`

6. Suffix plugin libraries with `-plugin`: When referencing a plugin as a library (not in the `[plugins]` section), append `-plugin` to the name.

   Example: For `org.owasp:dependency-check-gradle`, use `dependency-check-plugin`

### Example

gradle/libs.versions.toml

    [versions]
    slf4j = "2.0.13"
    jackson = "2.17.1"
    groovy = "3.0.5"
    checkstyle = "8.37"
    commonsLang = "3.9"

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

    # Jackson
    jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
    jackson-dataformatCsv = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-csv", version.ref = "jackson" }

    # Groovy bundle
    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" }

    # Apache Commons Lang
    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]
    versions = { id = "com.github.ben-manes.versions", version = "0.45.0" }

build.gradle.kts

    plugins {
        id("java-library")
        alias(libs.plugins.versions)
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        // SLF4J
        implementation(libs.slf4j.api)

        // Jackson
        implementation(libs.jackson.databind)
        implementation(libs.jackson.dataformatCsv)

        // Groovy bundle
        api(libs.bundles.groovy)

        // Commons Lang
        implementation(libs.commons.lang3)
    }

build.gradle

    plugins {
        id 'java-library'
        alias(libs.plugins.versions)
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        // SLF4J
        implementation libs.slf4j.api

        // Jackson
        implementation libs.jackson.databind
        implementation libs.jackson.dataformatCsv

        // Groovy bundle
        api libs.bundles.groovy

        // Commons Lang
        implementation libs.commons.lang3
    }

### References

- Best Practices for Naming Gradle Version Catalog Entries

### Tags

`#version-catalog`

## Set up your Dependency Repositories in the Settings file

Declare your repositories for your plugins and dependencies in `settings.gradle.kts`.

### Explanation

Using `settings.gradle.kts` file to declare repositories has several benefits:

- Avoids repetition: Centralizing repository declarations eliminates the need to repeat them in each project’s `build.gradle.kts`.

- Improves debuggability: Ensures all projects resolve dependencies during resolution from the same repositories, in a consistent order.

- Matches the build model: Repositories are not part of the project definition; they are part of global build logic, so settings is a more appropriate place for them.

| While `dependencyResolutionManagement.repositories` is an incubating API, it is the preferred way of declaring repositories.

### Example

#### Don’t Do This

You could set up repositories in individual `build.gradle.kts` files with:

build.gradle.kts

    buildscript {
        repositories {
            mavenCentral()
            gradlePluginPortal()
        }
    }

    plugins {
        id("java")
    }

    repositories {
        mavenCentral()
    }

build.gradle

    buildscript {
        repositories {
            mavenCentral()
            gradlePluginPortal()
        }
    }

    plugins {
        id("java")
    }

    repositories {
        mavenCentral()
    }

#### Do This Instead

Instead, you should set them up in `settings.gradle.kts` like this:

settings.gradle.kts

    pluginManagement {
        repositories {
            mavenCentral()
            gradlePluginPortal()
        }
    }
    dependencyResolutionManagement {
        repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
        repositories {
            mavenCentral()
        }
    }

settings.gradle

    pluginManagement {
        repositories {
            mavenCentral()
            gradlePluginPortal()
        }
    }
    dependencyResolutionManagement {
        repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
        repositories {
            mavenCentral()
        }
    }

### Tags

`#structuring-builds`

## Don’t Explicitly Depend on the Kotlin Standard Library

The Kotlin Gradle Plugin automatically adds a dependency on the Kotlin standard library (`stdlib`) to each source set, so there is no need to declare it explicitly.

### Explanation

The version of the standard library added is the same as the version of the Kotlin Gradle Plugin applied to the project. If your build does not require a specific or different version of the standard library, you should avoid adding it manually.

| Setting the `kotlin.stdlib.default.dependency` property to `false` prevents the Kotlin plugin from automatically adding the Kotlin standard library dependency to your project. This can be useful in specific scenarios, such as when you want to manage the Kotlin standard library dependency version manually.

### Example

#### Don’t Do This

build.gradle.kts

    plugins {
        kotlin("jvm").version("2.0.21")
    }

    dependencies {
        api(kotlin("stdlib")) (1)
    }

build.gradle

    plugins {
        id("org.jetbrains.kotlin.jvm") version "2.0.21"
    }

    dependencies {
        api("org.jetbrains.kotlin:kotlin-stdlib:2.0.21") (1)
    }

1 | `stdlib` is explicitly depended upon: This project contains an implicit dependency on the Kotlin standard library, which is required to compile its source code.

#### Do This Instead

build.gradle.kts

    plugins {
        kotlin("jvm").version("2.0.21") (1)
    }

build.gradle

    plugins {
        id("org.jetbrains.kotlin.jvm") version "2.0.21"  (1)
    }

1 | `stdlib` dependency is not included explicitly: The standard library remains available for use, and source code requiring it can be compiled without any issues.

### References

- the `kotlin()` function

### Tags

`#dependencies`

## Avoid Redundant Dependency Declarations

Avoid declaring the same dependency multiple times, especially when it is already available transitively or through another configuration.

### Explanation

Duplicating dependencies in Gradle build scripts can lead to:

- Increased maintenance: Declaring a dependency in multiple places makes it harder to manage.

- Unexpected behavior: Declaring the same dependency in multiple configurations (e.g., `compileOnly` and `implementation`) can result in hard-to-diagnose classpath issues.

### Example

#### Don’t Do This

build.gradle.kts

    plugins {
        `java-library`
    }

    dependencies {
        api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0")
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") (1)
    }

build.gradle

    plugins {
        id 'java-library'
    }

    dependencies {
        api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0")
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") (1)
    }

1 | Redundant dependency in `implementation` scope.

#### Do This Instead

build.gradle.kts

    plugins {
        `java-library`
    }

    dependencies {
        api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") (1)
    }

build.gradle

    plugins {
        id 'java-library'
    }

    dependencies {
        api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") (1)
    }

1 | Declare dependency once

### Tags

`#dependencies`

## Declare Dependencies using a single GAV (`group:artifact:version`) String

When declaring dependencies without a version catalog, prefer using the single GAV string notation `implementation("org.example:library:1.0")`. Avoid using the named argument notation unless necessary `implementation(group = "org.example", name = "library", version = "1.0")`.

| The named argument and map notations may be deprecated in the future.

### Explanation

All of these declarations will be treated equivalently when Gradle resolves dependencies. However, the single-string form is more concise, easier to read, and is widely adopted in the broader JVM ecosystem.

This format is also recommended by Maven Central in its documentation and usage examples, making it the most familiar and consistent style for developers across tools.

A valid reason to use named arguments format is when resolving a dependency from an Ivy repository that requires selecting a specific configuration by name. This syntax is only applicable for Ivy dependencies and should not be used with standard Maven modules:

    // Use this syntax only for Ivy modules when a specific configuration must be selected
    dependencies {
        implementation(group = "org.someOrg", name = "someModule", version = "1.0", configuration = "someConf")
    }

### Example

#### Don’t Do This

build.gradle.kts

    dependencies {
        implementation(group = "com.fasterxml.jackson.core", name = "jackson-databind", version = "32.17.0")  (1)
        api(group = "com.google.guava", name = "guava", version = "32.1.2-jre") {
            exclude(group = "com.google.code.findbugs", module = "jsr305")  (2)
        }
    }

build.gradle

    dependencies {
        implementation(group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.17.0') (1)
        api(group: 'com.google.guava', name: 'guava', version: '32.1.2-jre') {
            exclude(group: 'com.google.code.findbugs', module: 'jsr305')    (2)
        }
    }

1 | Avoid the named argument notation when declaring dependencies
2 | Other modifiers methods and constraints like `exclude` are not included in this recommendation and can use named argument notation as needed

#### Do This Instead

build.gradle.kts

    dependencies {
        implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") (1)
        api("com.google.guava:guava:32.1.2-jre") {
            exclude(group = "com.google.code.findbugs", module = "jsr305")  (2)
        }
    }

build.gradle

    dependencies {
        implementation('com.fasterxml.jackson.core:jackson-databind:2.17.0') (1)
        api('com.google.guava:guava:32.1.2-jre') {
            exclude(group: 'com.google.code.findbugs', module: 'jsr305')    (2)
        }
    }

1 | Use the string notation instead when declaring dependencies
2 | Other modifiers methods and constraints like `exclude` are not included in this recommendation and can use named argument notation as needed

### References

- Declaring Dependencies Basics

### Tags

`#dependencies`