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

dependency management 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:

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.