skip to content

What is a rich version declaration in Gradle, and what problem does it solve compared to a plain version string?

level: juniorimportance: must knowfreq 55%

answer

  1. version { } block
  2. require / prefer / strictly / reject
  3. string = preference, upgradeable
  4. strictly fails the build
  5. works on dependency and constraint

basics

~20 s

A rich version uses a version { } block to declare more than a single string — e.g. require, prefer, strictly, reject — so you can express acceptable ranges and preferences instead of just one fixed number.

solid answer

~40 s

A plain `version` is a single string Gradle treats as a *preference* — it can be upgraded by conflict resolution. A **rich version** uses a `version { }` block to declare intent more precisely with several keywords: `require` (the minimum acceptable, expressed as a range), `prefer` (a hint chosen when the range allows it), `strictly` (a hard constraint that *fails the build* if something else wants outside it), and `reject` / `rejectAll` (versions to exclude). This lets you say things like "any 1.x but never 1.4 due to a CVE, and prefer 1.6". It solves the problem that a single version string can't distinguish "I prefer this" from "this is mandatory", which matters once transitive dependencies pull conflicting versions and Gradle has to pick a winner.

code

kotlin · 9 lines
kotlin
dependencies {
    implementation("com.squareup.okhttp3:okhttp") {
        version {
            require("[4.9, 5.0[")
            prefer("4.12.0")
            reject("4.10.0") // regression
        }
    }
}

go deeper

for a junior

Know the four keywords exist and that a plain version is only a preference Gradle can upgrade.

for a middle

Explain the priority order (strictly > require > prefer) and that strictly fails the build rather than upgrading.

for a senior

Discuss using rich versions on constraints/platforms and that they publish into Gradle Module Metadata for consumers.

for a principal

Frame rich versions as a governance tool: org-wide platforms expressing strictly/reject policies, CVE blacklisting, and reproducibility guarantees.

## The problem with a single version string When you write `implementation("org.example:lib:1.5")`, Gradle does **not** treat `1.5` as a command. It treats it as a *preferred* version. During conflict resolution, if a transitive dependency requires `1.7`, Gradle picks the **highest** version, and your `1.5` is silently upgraded. A bare string can't express the difference between "I'd like 1.5" and "it MUST be 1.5". ## Rich versions A **rich version declaration** replaces the string with a `version { }` block that supports four kinds of intent: - **`require`** — the acceptable version *range* (a minimum / set of allowed versions). This is what a plain string maps to. Can still be upgraded by resolution. - **`prefer`** — a soft hint: used only when nothing else forces a choice within the allowed range. Lower priority than `require`. - **`strictly`** — a hard constraint. If any other participant in the graph needs a version outside this range, the build **fails** instead of silently upgrading. This is how you *pin* or *downgrade*. - **`reject`** — specific versions or ranges that are never acceptable (e.g. a known-bad release). **`rejectAll()`** rejects everything (useful to forbid a dependency entirely via a constraint). ## Where it can be used The `version { }` block is available both on a **dependency** declaration and on a **dependency constraint** (in the `constraints { }` block, or via a platform). On a constraint it does not *add* the dependency to the graph; it only influences the version *if* the dependency appears. ```kotlin dependencies { implementation("org.example:lib") { version { require("[1.0, 2.0[") // any 1.x prefer("1.6") // pick 1.6 if the range allows reject("1.4") // never 1.4 (bad release) } } } ``` ## Why it matters Rich versions move you from "hope resolution picks the right thing" to *declaring intent*. `strictly` gives reproducibility and lets you downgrade a transitive dependency; `reject` lets you blacklist a CVE-affected release; `prefer` lets you nudge without forcing. They are also what get published into Gradle Module Metadata so consumers see your constraints.

  • If you only set `require("1.5")`, can Gradle still resolve to 1.7?
    Yes. `require` (like a plain string) is upgradeable by conflict resolution, so a transitive `1.7` would win. Use `strictly` to prevent that.
  • Does a rich version on a constraint add the dependency to the graph?
    No. A constraint only influences the version *if* the dependency is already pulled in (directly or transitively); it doesn't introduce it.

saying these in an interview costs you the question

  • Claiming a plain version string is a hard pin that resolution always respects
  • Saying `prefer` overrides `require` (it's lower priority)
  • Thinking rich versions are only usable on direct dependencies, not constraints

context