# Declaring Dependency Constraints

version 9.8.0-milestone-2

Version declarations (including ranges) only affect the dependencies you declare directly. To control versions of transitive dependencies, use dependency constraints.

A dependency constraint sets version requirements for a module without adding that module as a dependency. When the module is pulled in, the constraint participates in version conflict resolution just like a declared dependency version:

- Constraints are not strict by default (they usually express "at least this version").

- You can make them strict or use rich versions (e.g., ranges, `prefer`, `reject`, `strictly`) when needed.

| Use constraints when you want to control versions centrally and avoid adding extra dependencies just to force a version.

You can declare constraints:

- Alongside dependencies in a single project (scoped to the same configuration buckets like `implementation`, `runtimeOnly`, `testImplementation`, etc.).

  build.gradle.kts

      dependencies {
          implementation("com.google.guava:guava")

          constraints {
              implementation("com.google.guava:guava:33.0.0-jre")
          }
      }

  build.gradle

      dependencies {
          implementation 'com.google.guava:guava'

          constraints {
              implementation 'com.google.guava:guava:33.0.0-jre'
          }
      }

- Centrally in a platform (recommended for multi-project builds) using the `java-platform` plugin:

  build.gradle.kts

      plugins {
          `java-platform`
      }

      dependencies {
          constraints {
              api("com.google.guava:guava:33.0.0-jre")
              api("com.fasterxml.jackson.core:jackson-databind:2.16.1")
              api("org.slf4j:slf4j-api:2.0.9")
          }
      }

  build.gradle

      plugins {
          id 'java-platform'
      }

      dependencies {
          constraints {
              api 'com.google.guava:guava:33.0.0-jre'
              api 'com.fasterxml.jackson.core:jackson-databind:2.16.1'
              api 'org.slf4j:slf4j-api:2.0.9'
          }
      }

You can use version catalog entries as constraints, and those entries can carry rich versions, including `strictly`, `prefer`, and `reject`.

## Declaring constraints alongside direct dependencies

Constraints are scoped by configurations (e.g., `implementation`, `runtimeOnly`). They apply whenever that dependency is encountered during resolution.

The `constraints {}` block is used within the `dependencies {}` block to declare these constraints:

build.gradle.kts

    dependencies {
        implementation("com.fasterxml.jackson.core:jackson-databind")

        constraints {
            implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1") {
                because("tested with this version")
            }
        }
    }

build.gradle

    dependencies {
        implementation 'com.fasterxml.jackson.core:jackson-databind'

        constraints {
            implementation('com.fasterxml.jackson.core:jackson-databind:2.16.1') {
                because 'tested with this version'
            }
        }
    }

Here, `jackson-databind` is declared without a version. The constraint ensures that when it is resolved, Gradle selects at least version `2.16.1`:

    runtimeClasspath - Runtime classpath of source set 'main'.
    +--- com.fasterxml.jackson.core:jackson-databind -> 2.16.1
    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.16.1
    |    |    \--- com.fasterxml.jackson:jackson-bom:2.16.1
    |    |         +--- com.fasterxml.jackson.core:jackson-annotations:2.16.1 (c)
    |    |         +--- com.fasterxml.jackson.core:jackson-core:2.16.1 (c)
    |    |         \--- com.fasterxml.jackson.core:jackson-databind:2.16.1 (c)
    |    +--- com.fasterxml.jackson.core:jackson-core:2.16.1
    |    |    \--- com.fasterxml.jackson:jackson-bom:2.16.1 (*)
    |    \--- com.fasterxml.jackson:jackson-bom:2.16.1 (*)
    \--- com.fasterxml.jackson.core:jackson-databind:2.16.1 (c)

If multiple constraints or dependencies require different versions of the same module, Gradle picks a version that satisfies all. If none exists, resolution fails with an error describing the conflict.

## Adding constraints on transitive dependencies

Use constraints to select transitive modules without introducing them as direct dependencies:

build.gradle.kts

    dependencies {
        implementation("org.apache.httpcomponents:httpclient:4.5.13")

        constraints {
            implementation("commons-codec:commons-codec:1.15") {
                because("httpclient pulls in 1.11 which has known vulnerabilities")
            }
        }
    }

build.gradle

    dependencies {
        implementation 'org.apache.httpcomponents:httpclient:4.5.13'

        constraints {
            implementation('commons-codec:commons-codec:1.15') {
                because 'httpclient pulls in 1.11 which has known vulnerabilities'
            }
        }
    }

Here, `httpclient:4.5.13` pulls in `commons-codec:1.11` as a transitive dependency. The constraint upgrades it to `1.15` without adding `commons-codec` as a direct dependency:

    runtimeClasspath - Runtime classpath of source set 'main'.
    +--- org.apache.httpcomponents:httpclient:4.5.13
    |    +--- org.apache.httpcomponents:httpcore:4.4.13
    |    +--- commons-logging:commons-logging:1.2
    |    \--- commons-codec:commons-codec:1.11 -> 1.15
    \--- commons-codec:commons-codec:1.15 (c)

If `commons-codec` isn’t brought in transitively, the constraint is a no-op (it doesn’t add the module). If it is brought in, the constraint guides the version selection.

## Transitivity and precedence of constraints

Dependency constraints are transitive.

If `library A` depends on `library B`, and `library B` declares a constraint on `module C`, that constraint will affect the version of `module C` that `library A` resolves:

    library A
    ├── library B
    │   └── constraint: module C >= 3  (1)
    └── module C:2                     (2)

1 | `library B` declares a constraint requiring `module C` version `3` or higher.
2 | `library A` depends on `module C` version `2` directly, but the transitive constraint from `library B` upgrades it to version `3`.

## Rich versions for constraints

By default, a constraint expresses "at least this version", so Gradle can still upgrade past it.

For tighter control, attach a rich version to the constraint:

- `strictly` — pins to an exact version (or range); no other dependency can upgrade past it.

- `prefer` — indicates the preferred version, but allows Gradle to select a different one if needed.

- `reject` — excludes specific versions from consideration.

For example, using `strictly` to prevent any upgrade past a known-good version:

build.gradle.kts

    dependencies {
        constraints {
            implementation("com.google.guava:guava") {
                version {
                    strictly("33.1.0-jre")
                }
                because("avoid older versions with known issues")
            }
        }
    }

build.gradle

    dependencies {
        constraints {
            implementation("com.google.guava:guava") {
                version {
                    strictly("33.1.0-jre")
                }
                because("avoid older versions with known issues")
            }
        }
    }

### Competing `strictly` declarations

When more than one `strictly` declaration applies to the same dependency, Gradle’s behavior depends on where those declarations sit in the dependency graph. Same-level declarations compose by intersection; declarations at different depths follow a precedence rule.

#### Same level: intersection

When multiple `strictly` declarations on the same module are made at the same level, for example, a direct dependency and a constraint in the same project, or two constraints side by side, Gradle takes the intersection of their accepted versions and resolves to the highest version inside it.

A direct dependency with a wide strict range, combined with a constraint that narrows it:

build.gradle.kts

    dependencies {
        // Direct dependency with a wide strict range.
        api("org.apache.httpcomponents:httpclient") {
            version {
                strictly("[4.0, 5.0[")
            }
        }
        constraints {
            // Same module, narrower strict range that is fully inside the direct declaration's range.
            // If Gradle takes the intersection, the resolved version comes from [4.3, 4.5[ (some 4.4.x).
            // If the direct declaration overrides the constraint, the resolved version comes from [4.0, 5.0[ (4.5.14).
            // If same-level conflicts fail, the build fails.
            api("org.apache.httpcomponents:httpclient") {
                version {
                    strictly("[4.3, 4.5[")
                }
            }
        }
    }

build.gradle

    dependencies {
        // Direct dependency with a wide strict range.
        api('org.apache.httpcomponents:httpclient') {
            version {
                strictly '[4.0, 5.0['
            }
        }
        constraints {
            // Same module, narrower strict range that is fully inside the direct declaration's range.
            // If Gradle takes the intersection, the resolved version comes from [4.3, 4.5[ (some 4.4.x).
            // If the direct declaration overrides the constraint, the resolved version comes from [4.0, 5.0[ (4.5.14).
            // If same-level conflicts fail, the build fails.
            api('org.apache.httpcomponents:httpclient') {
                version {
                    strictly '[4.3, 4.5['
                }
            }
        }
    }

The direct declaration’s `[4.0, 5.0[` intersected with the constraint’s `[4.3, 4.5[` gives `[4.3, 4.5[`; the resolved version is the highest available there:

    runtimeClasspath - Runtime classpath of source set 'main'.
    +--- org.apache.httpcomponents:httpclient:{strictly [4.0, 5.0[} -> 4.4.1
    \--- org.apache.httpcomponents:httpclient:{strictly [4.3, 4.5[} -> 4.4.1 (c)

The intersection behavior is the same when both declarations are constraints rather than one being a direct dependency:

build.gradle.kts

    dependencies {
        constraints {
            // Two overlapping strict ranges declared as constraints in the same project.
            // If intersection applies, resolved version is in [4.3, 4.5[.
            // If one constraint silently wins, resolved version is in [4.0, 5.0[ or [4.3, 4.5[ alone.
            api("org.apache.httpcomponents:httpclient") {
                version {
                    strictly("[4.0, 5.0[")
                }
            }
            api("org.apache.httpcomponents:httpclient") {
                version {
                    strictly("[4.3, 4.5[")
                }
            }
        }
    }

build.gradle

    dependencies {
        constraints {
            // Two overlapping strict ranges declared as constraints in the same project.
            // If intersection applies, resolved version is in [4.3, 4.5[.
            // If one constraint silently wins, resolved version is in [4.0, 5.0[ or [4.3, 4.5[ alone.
            api('org.apache.httpcomponents:httpclient') {
                version {
                    strictly '[4.0, 5.0['
                }
            }
            api('org.apache.httpcomponents:httpclient') {
                version {
                    strictly '[4.3, 4.5['
                }
            }
        }
    }

Both constraints are honored, and the resolved version satisfies both:

    runtimeClasspath - Runtime classpath of source set 'main'.
    +--- org.apache.httpcomponents:httpclient:4.4
    +--- org.apache.httpcomponents:httpclient:{strictly [4.0, 5.0[} -> 4.4 (c)
    \--- org.apache.httpcomponents:httpclient:{strictly [4.3, 4.5[} -> 4.4 (c)

If the two same-level ranges do not overlap, the intersection is empty and resolution fails:

build.gradle.kts

    dependencies {
        api("org.apache.httpcomponents:httpclient") {
            version {
                strictly("[4.0, 4.5[")  // 4.0 through 4.4.x
            }
        }
        constraints {
            api("org.apache.httpcomponents:httpclient") {
                version {
                    strictly("[4.5, 5.0[")  // 4.5 through 4.5.14 — disjoint from above
                }
            }
        }
    }

build.gradle

    dependencies {
        api('org.apache.httpcomponents:httpclient') {
            version {
                strictly '[4.0, 4.5['  // 4.0 through 4.4.x
            }
        }
        constraints {
            api('org.apache.httpcomponents:httpclient') {
                version {
                    strictly '[4.5, 5.0['  // 4.5 through 4.5.14 — disjoint from above
                }
            }
        }
    }

The dependency report marks both declarations as `FAILED`:

    runtimeClasspath - Runtime classpath of source set 'main'.
    +--- org.apache.httpcomponents:httpclient:{strictly [4.0, 4.5[} FAILED
    \--- org.apache.httpcomponents:httpclient:{strictly [4.5, 5.0[} FAILED

#### Across graph depths: the root-most wins outright

When the competing `strictly` declarations sit at different depths in the dependency graph, Gradle does not take the intersection. The declaration closer to the root fully overrides the deeper one, even when the deeper declaration’s range is more restrictive and would have produced a version that satisfies both.

In this example, the root project declares a wide strict range as a direct dependency:

build.gradle.kts

    dependencies {
        api(project(":moduleA"))
        api("org.apache.httpcomponents:httpclient") {
            version {
                // Wide strict range: max is in 4.5.x.
                strictly("[4.0, 5.0[")
            }
        }
    }

build.gradle

    dependencies {
        api(project(":moduleA"))
        api('org.apache.httpcomponents:httpclient') {
            version {
                // Wide strict range: max is in 4.5.x.
                strictly '[4.0, 5.0['
            }
        }
    }

A subproject contributes a narrower strict range, fully inside the root’s:

build.gradle.kts

    dependencies {
        constraints {
            api("org.apache.httpcomponents:httpclient") {
                version {
                    // Narrower strict range, fully inside the root's range.
                    // Intersection rule predicts the resolved version comes from this range (4.4.x).
                    // Root-wins-unconditionally predicts the resolved version comes from root's range (4.5.x).
                    strictly("[4.3, 4.5[")
                }
            }
        }
    }

build.gradle

    dependencies {
        constraints {
            api('org.apache.httpcomponents:httpclient') {
                version {
                    // Narrower strict range, fully inside the root's range.
                    // Intersection rule predicts the resolved version comes from this range (4.4.x).
                    // Root-wins-unconditionally predicts the resolved version comes from root's range (4.5.x).
                    strictly '[4.3, 4.5['
                }
            }
        }
    }

The resolved version comes from the root’s range only:

    runtimeClasspath - Runtime classpath of source set 'main'.
    +--- project ':moduleA'
    |    \--- org.apache.httpcomponents:httpclient:{strictly [4.3, 4.5[} -> 4.5.14 (c)
    \--- org.apache.httpcomponents:httpclient:{strictly [4.0, 5.0[} -> 4.5.14

The selection `4.5.14` is the maximum of the root’s `[4.0, 5.0[` range, and it is outside the subproject’s `[4.3, 4.5[` strict declaration. The subproject’s `strictly` was silently dropped.

The same rule applies when the ranges do not overlap at all, the deeper `strictly` is dropped rather than causing a conflict:

build.gradle.kts

    dependencies {
        api(project(":moduleA"))
        api("org.apache.httpcomponents:httpclient") {
            version {
                strictly("4.4")  // This constraint is honored
            }
        }
    }

build.gradle

    dependencies {
        api(project(':moduleA'))
        api('org.apache.httpcomponents:httpclient') {
            version {
                strictly '4.4'  // This constraint is honored
            }
        }
    }

moduleA/build.gradle.kts

    dependencies {
        constraints {
            api("org.apache.httpcomponents:httpclient") {
                version {
                    strictly("4.5")  // This constraint is ignored
                }
            }
        }
    }

moduleA/build.gradle

    dependencies {
        constraints {
            api('org.apache.httpcomponents:httpclient') {
                version {
                    strictly '4.5'  // This constraint is ignored
                }
            }
        }
    }

The root’s pinned `4.4` wins over the subproject’s `4.5`, and the subproject’s declaration is dropped instead of triggering a conflict failure:

    runtimeClasspath - Runtime classpath of source set 'main'.
    +--- project ':moduleA'
    |    \--- org.apache.httpcomponents:httpclient:{strictly 4.5} -> 4.4 (c)
    \--- org.apache.httpcomponents:httpclient:{strictly 4.4} -> 4.4
         +--- org.apache.httpcomponents:httpcore:4.4
         +--- commons-logging:commons-logging:1.2
         \--- commons-codec:commons-codec:1.9

The rule also holds for any depth — root-most wins, all deeper `strictly` declarations are dropped together. This three-level example has the root, a direct subproject, and a transitive subproject each declaring a `strictly` on the same module:

build.gradle.kts

    dependencies {
        api(project(":moduleA"))
        api("org.apache.httpcomponents:httpclient") {
            version {
                // Level 1 (root): widest strict range.
                strictly("[4.0, 5.0[")
            }
        }
    }

build.gradle

    dependencies {
        api(project(":moduleA"))
        api('org.apache.httpcomponents:httpclient') {
            version {
                // Level 1 (root): widest strict range.
                strictly '[4.0, 5.0['
            }
        }
    }

build.gradle.kts

    dependencies {
        api(project(":moduleB"))
        constraints {
            api("org.apache.httpcomponents:httpclient") {
                version {
                    // Level 2 (subproject): narrower strict range.
                    strictly("[4.3, 4.5[")
                }
            }
        }
    }

build.gradle

    dependencies {
        api(project(":moduleB"))
        constraints {
            api('org.apache.httpcomponents:httpclient') {
                version {
                    // Level 2 (subproject): narrower strict range.
                    strictly '[4.3, 4.5['
                }
            }
        }
    }

build.gradle.kts

    dependencies {
        constraints {
            api("org.apache.httpcomponents:httpclient") {
                version {
                    // Level 3 (transitive subproject): even narrower, pinned to one minor.
                    strictly("[4.3, 4.4[")
                }
            }
        }
    }

build.gradle

    dependencies {
        constraints {
            api('org.apache.httpcomponents:httpclient') {
                version {
                    // Level 3 (transitive subproject): even narrower, pinned to one minor.
                    strictly '[4.3, 4.4['
                }
            }
        }
    }

Both `moduleA’s and `moduleB’s strict declarations are dropped; the root’s range alone governs the result:

    runtimeClasspath - Runtime classpath of source set 'main'.
    +--- project ':moduleA'
    |    +--- project ':moduleB'
    |    |    \--- org.apache.httpcomponents:httpclient:{strictly [4.3, 4.4[} -> 4.5.14 (c)
    |    \--- org.apache.httpcomponents:httpclient:{strictly [4.3, 4.5[} -> 4.5.14 (c)
    \--- org.apache.httpcomponents:httpclient:{strictly [4.0, 5.0[} -> 4.5.14

| Gradle’s conflict-resolution rules differ between same-level and cross-depth `strictly` declarations. At the same level, declarations compose by intersection (or fail when the intersection is empty). Across depths, the root-most declaration takes complete precedence and any deeper `strictly` is silently dropped. This protects a project’s right to control the versions of its direct dependencies without negotiation with the components it depends on.

#### Overriding `strictly` from the resolution strategy

The precedence rules above govern how competing `strictly` declarations resolve against one another within the graph. Sitting above all of them, a project keeps one final escape hatch: the configuration’s resolution strategy.

Both of the following override a `strictly` constraint outright, and can select a version outside its accepted range:

- `resolutionStrategy.force(…​)` replaces the resolved version regardless of any `strictly` declaration in the graph.

- A dependency resolve rule (`resolutionStrategy.eachDependency { …​ useVersion(…​) }`) likewise wins over `strictly`.

| This is why "a strict version cannot be overridden" holds only within the dependency graph. Other participants in the graph cannot override a project’s `strictly`, but the consuming project can always override it through its own resolution strategy. Prefer `strictly` to express version intent — it is published in Gradle Module Metadata and participates in conflict resolution — and reserve `force` and resolve rules for local, last-resort overrides.

### `strictly` constraints from platforms

When a platform declares a `strictly` constraint and a consumer imports it with `platform()`, the strict version is endorsed by default; Gradle treats it as if the consumer declared it directly.

A platform that pins `guava` to an exact version:

build.gradle.kts

    plugins {
        `java-platform`
    }

    dependencies {
        constraints {
            api("com.google.guava:guava") {
                version {
                    strictly("33.0.0-jre")
                }
                because("platform pins guava to a tested version")
            }
        }
    }

build.gradle

    plugins {
        id 'java-platform'
    }

    dependencies {
        constraints {
            api('com.google.guava:guava') {
                version {
                    strictly '33.0.0-jre'
                }
                because 'platform pins guava to a tested version'
            }
        }
    }

A consumer that imports the platform and depends on `guava`:

build.gradle.kts

    dependencies {
        // platform() endorses strict versions by default
        implementation(platform(project(":platform")))

        // No version needed — the platform's strictly constraint pins guava to 33.0.0-jre
        implementation("com.google.guava:guava")
    }

build.gradle

    dependencies {
        // platform() endorses strict versions by default
        implementation platform(project(':platform'))

        // No version needed — the platform's strictly constraint pins guava to 33.0.0-jre
        implementation 'com.google.guava:guava'
    }

The dependency tree confirms that `guava` resolves to `33.0.0-jre`, the version pinned by the platform:

    runtimeClasspath - Runtime classpath of source set 'main'.
    +--- project ':platform'
    |    \--- com.google.guava:guava:{strictly 33.0.0-jre} -> 33.0.0-jre (c)
    \--- com.google.guava:guava -> 33.0.0-jre

If the consumer or any transitive dependency requests a version outside the `strictly` constraint (e.g., `guava:33.1.0-jre`), resolution will fail.

To disable this behavior, use `doNotEndorseStrictVersions()` on the platform dependency, see Strict Version Endorsement.

## Publishing constraints

Dependency constraints are only published when using Gradle Module Metadata. This means they are fully supported only when both publishing and consuming modules with Gradle.

If modules are consumed with Maven or Ivy, the constraints may not be preserved.