skip to content

How do you use `strictly` to force a downgrade of a transitive dependency, and what happens if another part of the graph disagrees?

level: middleimportance: must knowfreq 50%

answer

  1. strictly = hard constraint, enables downgrade
  2. highest-wins is the default
  3. conflict outside range = build fails
  4. stronger than resolutionStrategy.force
  5. pair with because() and constraints

basics

~20 s

Declare the dependency (or a constraint) with version { strictly("1.2") }. strictly is a hard upper bound, so Gradle will downgrade to it. If another node needs a version outside the strict range, the build fails with a conflict.

solid answer

~40 s

Normal conflict resolution always picks the **highest** requested version, so you can't downgrade a transitive dependency just by requesting a lower number. `strictly` changes that: it declares a *hard* constraint. `version { strictly("1.2") }` (or a strict range like `strictly("[1.2, 1.3[")`) tells Gradle that nothing outside that range is acceptable, which lets you **force a downgrade** of a version some transitive dependency pulled in higher. The trade-off: if another participant in the graph *strictly* needs something incompatible — or directly requires a version outside your strict range that can't be reconciled — Gradle **fails the build** with a version-conflict error rather than silently picking one. To downgrade safely you often pair `strictly` with a `because("...")` reason and apply it on a constraint so it only bites when the dependency is actually present.

code

kotlin · 10 lines
kotlin
dependencies {
    constraints {
        implementation("com.google.guava:guava") {
            version {
                strictly("32.1.3-jre")
            }
            because("newer guava pulled transitively breaks our API")
        }
    }
}

go deeper

for a junior

Know strictly pins/downgrades and that a plain lower version won't downgrade because highest wins.

for a middle

Explain that strictly enables downgrade, fails on conflict, and pairs with because(); know it's stronger than force.

for a senior

Discuss applying strictly via constraints/platforms and the consequences of publishing strict constraints in library metadata.

for a principal

Frame strict pinning as policy: centralized platforms, CVE remediation, and the downstream blast radius of strict constraints in published artifacts.

## Default resolution picks the highest Gradle's conflict resolution is "highest version wins". If you depend on `lib:1.5` and a transitive edge requires `lib:1.8`, you get `1.8`. Requesting `1.2` won't help — `1.2 < 1.8`, so it loses. ## strictly forces the outcome `strictly` declares that versions outside the given range are **unacceptable**. Because it's a hard constraint, Gradle is now allowed to resolve to a version *lower* than another requested version — i.e. it enables downgrades, which `require`/plain strings cannot do. ```kotlin dependencies { implementation("org.apache.commons:commons-lang3") { version { strictly("3.12.0") } because("3.13 broke our reflection usage") } } ``` Or as a **constraint**, so it only applies if the dependency appears transitively: ```kotlin dependencies { constraints { implementation("com.fasterxml.jackson.core:jackson-databind") { version { strictly("2.15.4") } because("CVE in 2.16 line for our usage") } } } ``` ## What if the graph disagrees? If another module **strictly** requires an incompatible version, or directly/forcibly requires a version outside your strict range, the resolution is unsatisfiable and Gradle **fails** with a message naming the conflicting paths. This is intentional: a strict conflict is a real problem you must resolve explicitly (loosen one side, exclude, or align via a platform). Note `strictly` is *stronger* than the old `force = true` / `resolutionStrategy.force` and is the modern, metadata-publishable way to pin. ## strictly vs a strict range `strictly("1.2")` pins exactly; `strictly("[1.2, 1.4[")` allows the resolver to pick within 1.2–1.3 but never 1.4+. Combine with `prefer` inside the same block to bias the choice within the strict window. ## Publishing When you publish a library, strict constraints are written into **Gradle Module Metadata** and *propagate* to consumers — be deliberate, because you're constraining everyone downstream, and a strict constraint in a published library can cause downstream build failures.

  • Why prefer strictly over resolutionStrategy.force?
    strictly is dependency-scoped, expresses intent declaratively, supports because() reasons, and is published in module metadata so consumers see it; force is a blunt global override that isn't published.
  • Can require ever downgrade a transitive version?
    No. require/plain strings only set a floor and lose to higher requested versions; only strictly (or force) can drive a downgrade.
  • What's a safer way to apply strictly so it doesn't add unwanted deps?
    Put it in a constraints { } block — it then only influences the version if the dependency is actually present in the graph.

require is like a minimum acceptable salary you'd still negotiate up; strictly is a non-negotiable cap in your contract — if the other party insists on more, the deal (build) is off.

saying these in an interview costs you the question

  • Saying you can downgrade just by requesting a lower plain version
  • Confusing strictly with prefer
  • Forgetting that a strict constraint in a published library propagates to consumers

context