Declaring Dependency Constraints
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.ktsdependencies { implementation("com.google.guava:guava") constraints { implementation("com.google.guava:guava:33.0.0-jre") } }build.gradledependencies { 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-platformplugin:build.gradle.ktsplugins { `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.gradleplugins { 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:
dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind")
constraints {
implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1") {
because("tested with this version")
}
}
}
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:
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")
}
}
}
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:
dependencies {
constraints {
implementation("com.google.guava:guava") {
version {
strictly("33.1.0-jre")
}
because("avoid older versions with known issues")
}
}
}
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:
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[")
}
}
}
}
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:
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[")
}
}
}
}
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:
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
}
}
}
}
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:
dependencies {
api(project(":moduleA"))
api("org.apache.httpcomponents:httpclient") {
version {
// Wide strict range: max is in 4.5.x.
strictly("[4.0, 5.0[")
}
}
}
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:
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[")
}
}
}
}
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:
dependencies {
api(project(":moduleA"))
api("org.apache.httpcomponents:httpclient") {
version {
strictly("4.4") // This constraint is honored
}
}
}
dependencies {
api(project(':moduleA'))
api('org.apache.httpcomponents:httpclient') {
version {
strictly '4.4' // This constraint is honored
}
}
}
dependencies {
constraints {
api("org.apache.httpcomponents:httpclient") {
version {
strictly("4.5") // This constraint is ignored
}
}
}
}
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:
dependencies {
api(project(":moduleA"))
api("org.apache.httpcomponents:httpclient") {
version {
// Level 1 (root): widest strict range.
strictly("[4.0, 5.0[")
}
}
}
dependencies {
api(project(":moduleA"))
api('org.apache.httpcomponents:httpclient') {
version {
// Level 1 (root): widest strict range.
strictly '[4.0, 5.0['
}
}
}
dependencies {
api(project(":moduleB"))
constraints {
api("org.apache.httpcomponents:httpclient") {
version {
// Level 2 (subproject): narrower strict range.
strictly("[4.3, 4.5[")
}
}
}
}
dependencies {
api(project(":moduleB"))
constraints {
api('org.apache.httpcomponents:httpclient') {
version {
// Level 2 (subproject): narrower strict range.
strictly '[4.3, 4.5['
}
}
}
}
dependencies {
constraints {
api("org.apache.httpcomponents:httpclient") {
version {
// Level 3 (transitive subproject): even narrower, pinned to one minor.
strictly("[4.3, 4.4[")
}
}
}
}
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 |
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 anystrictlydeclaration in the graph. -
A dependency resolve rule (
resolutionStrategy.eachDependency { … useVersion(…) }) likewise wins overstrictly.
|
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 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:
plugins {
`java-platform`
}
dependencies {
constraints {
api("com.google.guava:guava") {
version {
strictly("33.0.0-jre")
}
because("platform pins guava to a tested version")
}
}
}
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:
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")
}
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.