skip to content

A teammate published a library that declares a dependency as 'com.acme:lib:[1.0,2.0)'. A Maven consumer complains the POM is non-deterministic. What went wrong and how do you fix it at the coordinate/versioning layer?

level: seniorimportance: should knowfreq 30%

answer

  1. declared range copied to POM
  2. consumer re-resolves → non-deterministic
  3. versionMapping → resolved version
  4. allVariants { fromResolutionResult() }
  5. avoid publishing dynamic versions

basics

~20 s

The declared dynamic version range was copied verbatim into the published POM, so Maven re-resolves it and may pick a different version each build. Add versionMapping{} so the POM records the concrete resolved version instead.

solid answer

~40 s

By default Gradle puts the **declared** dependency version into the POM. A range like `[1.0,2.0)` is a dynamic version: Gradle resolved a concrete version at build time, but the POM still carried the range. A Maven consumer reading that POM re-resolves the range itself and can pick a different artifact on different days — non-deterministic and non-reproducible. The fix at this layer is `versionMapping {}`, which substitutes the **resolved** version (say `1.7.3`) into the POM. After that, the consumer sees a fixed version. The same fix covers `1.+` style and BOM-supplied (versionless) dependencies. Note this is about the dependency versions in the POM, not the library's own GAV — that should also avoid dynamic versions, but those are separate concerns. Best practice: avoid publishing dynamic versions, and always enable versionMapping for libraries.

code

kotlin · 6 lines
kotlin
create<MavenPublication>("lib") {
    from(components["java"])
    versionMapping {
        allVariants { fromResolutionResult() }
    }
}

go deeper

for a junior

May only recognize that version ranges are risky to depend on.

for a middle

Identify that the declared range leaked into the POM and that there's a way to publish resolved versions.

for a senior

Diagnose declared-vs-resolved precisely, apply versionMapping, and note Module Metadata vs POM-only consumer difference.

for a principal

Set org policy banning dynamic versions in published libraries and mandating versionMapping in the shared publishing convention plugin.

## Why the POM was non-deterministic Gradle, when generating a POM, copies the **declared** version of each dependency. If you declared a *dynamic version* — a range `[1.0,2.0)`, a prefix `1.+`, or `latest.release` — that exact dynamic string ends up in the published POM. Gradle's own build resolved the range to a concrete version at publish time, but it did not write that concrete version into the POM by default. So a downstream **Maven** consumer (or any tool that just reads the POM and re-resolves) evaluates the range *itself*, against *its* view of the repository, possibly on a later date — and may select a different artifact. Two builds, two results: non-reproducible. ## The coordinate/versioning-layer fix: versionMapping Enable `versionMapping {}` on the publication so the POM records the **resolved** version, not the declared range: ```kotlin publishing { publications { create<MavenPublication>("lib") { from(components["java"]) versionMapping { allVariants { fromResolutionResult() } } } } } ``` Now if the range resolved to `1.7.3` at publish time, the POM emits `<version>1.7.3</version>`. Every consumer gets that fixed version. This also fixes versionless BOM-supplied dependencies and `1.+`-style declarations. ## Defense in depth versionMapping makes the *output* deterministic, but the deeper hygiene is to **not publish dynamic versions** at all: - Pin dependency versions, or supply them via a platform/BOM with versionMapping enabled. - Avoid dynamic versions for your **own** module's `project.version` — a published library should have a fixed, immutable version (and `-SNAPSHOT` only during development). ## Distinguishing the two version concerns 1. **Your module's GAV version** (`project.version`) — should be a concrete release or a `-SNAPSHOT`. 2. **Your dependencies' versions in the POM** — fixed by versionMapping when you used ranges/BOMs/constraints. The complaint here is about (2). The clean library publishes concrete coordinates for itself and concrete dependency versions in its POM. ## What to say in an interview Name the cause (declared range copied into POM), name the fix (versionMapping → resolved versions), and add the best practice (avoid dynamic versions in published libraries).

  • Besides enabling versionMapping, what hygiene step prevents this class of problem?
    Avoid declaring dynamic versions (ranges, 1.+, latest.release) in libraries you publish; pin versions or use a BOM, and keep your own project.version concrete.
  • Does versionMapping help if the dependency version came from a BOM with no declared version?
    Yes — that is another case where the POM would otherwise have an empty version; versionMapping fills in the resolved one.
  • Would Gradle Module Metadata consumers have seen the same non-determinism?
    No; the .module metadata records resolved variant info, so Gradle-native consumers are unaffected. The problem is specific to POM-only (Maven) consumers.

saying these in an interview costs you the question

  • Blaming the Maven consumer instead of the publisher's POM containing a dynamic version.
  • Proposing to hardcode versions only in the consumer rather than fixing the published POM.
  • Confusing the module's own version with its dependency versions.

context