Maven Versions, SNAPSHOTs & Ranges

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Maven 3.10.0. Every example was run on JDK 26 on 2026-10-16 in a scratch workspace that mimics the tidepool build: a library io.tidepool:tidepool-data that is deployed as a SNAPSHOT, a library io.tidepool:tidepool-units that is deployed as several releases, and small consumer projects. Everything was published to a plain file:// directory that plays the role of a remote repository, so no real repository and no credentials were involved, and a scratch local repository was used so that nothing touched ~/.m2. Where Maven 3.9.16 or Maven 4.0.0-rc-7 (a release candidate) was run too, the text says so. Paths in the console output were shortened to $W (the scratch workspace), and the line Loaded N auto-discovered prefixes for remote repository ... that Maven 3.10 prints while resolving was left out.

A version in Maven is more than a label. It is an input to resolution: a version can be a fixed release, a moving SNAPSHOT that is replaced by a new build every day, or a range that is evaluated against whatever a repository happens to contain on the day of the build. This page explains each of those, the rules by which Maven orders versions, and what that means for repeatable builds. The coordinates themselves are on Philosophy, Coordinates & Directory Layout, how a version is chosen when two dependencies disagree is on Dependencies, Scopes & Mediation, and version properties such as ${revision} are on CI-Friendly Versions.

Release versus SNAPSHOT

A release version, such as 1.2 or 2.0-beta-1, names one immutable build. Once it is published, every consumer gets the same bytes for it, and a repository manager normally refuses to overwrite it. A SNAPSHOT version is a version that ends with the literal string -SNAPSHOT, such as 1.0-SNAPSHOT. It means "the development line leading to 1.0", a moving target that is rebuilt many times before the release. Because of that, Maven treats the two differently in three places:

  • Repositories. Each repository declares separately whether it serves releases and whether it serves snapshots, each with its own update policy (see Update policies and the -U flag). Maven Central serves releases only.

  • Deployment. A release is uploaded under its own version. A SNAPSHOT is uploaded under a timestamped name, described next.

  • Releasing. A project that depends on a SNAPSHOT cannot be released reproducibly, which is why the release process and the enforcer both refuse it (see Locking a snapshot and keeping snapshots out of releases).

The related coordinates of the build that resolve to a SNAPSHOT are in the POM, and the versions of the tools that read them are on Versions & Release History.

Deploying a SNAPSHOT: timestamps, build numbers and the baseVersion

The POM of tidepool-data has version 1.0-SNAPSHOT and a distributionManagement section pointing to a file:// repository (the section is the subject of Install, Deploy & Distribution Management):

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-data</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
  </properties>
  <distributionManagement>
    <repository>
      <id>local-release</id>
      <url>file:///path/to/workspace/repo</url>
    </repository>
    <snapshotRepository>
      <id>local-snapshot</id>
      <url>file:///path/to/workspace/repo</url>
    </snapshotRepository>
  </distributionManagement>
</project>

Maven chooses snapshotRepository for a SNAPSHOT version and repository for a release. The class Data had a method returning "v1". The library was deployed (mvn deploy), then the method was changed to return "v2" and the library was deployed again. This is the listing of the repository after the second deployment (the .md5 and .sha1 checksum files were left out):

repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/maven-metadata.xml
repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123510-1.jar
repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123510-1.pom
repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123517-2.jar
repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123517-2.pom
repo/io/tidepool/tidepool-data/maven-metadata.xml

No file is called tidepool-data-1.0-SNAPSHOT.jar. Maven replaced SNAPSHOT with the deployment time in UTC, 20261016.123517, and a build number, 2, that it increments from the metadata already in the repository (it starts at 1 for the first deployment). The transformation is 1.0-SNAPSHOT to 1.0-${YYYYMMDD.HHMMSS}-${counter}, and the timestamp is the time the Maven session was created, so every file of one deployment carries the same value. Each deployment adds files; it does not replace the previous ones. The Maven 3.10.0 log of the last step printed Skipping already-deployed io.tidepool:tidepool-data:1.0-SNAPSHOT for the deploy goal of the Deploy plugin 3.2.0 even though the files above had been uploaded; I did not investigate that message further, and the listing is what was verified.

The part of the version that the POM declares, 1.0-SNAPSHOT, is called the baseVersion. The timestamped 1.0-20261016.123517-2 is the version of the file. Two metadata files link them, and they are the reason a SNAPSHOT works at all.

The two metadata files

The version-level ("V level") metadata sits in the baseVersion directory and exists only in repositories that hold snapshots. It maps the baseVersion to the latest timestamped files:

<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1.0">
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-data</artifactId>
  <versioning>
    <lastUpdated>20261016123517</lastUpdated>
    <snapshot>
      <timestamp>20261016.123517</timestamp>
      <buildNumber>2</buildNumber>
    </snapshot>
    <snapshotVersions>
      <snapshotVersion>
        <extension>pom</extension>
        <value>1.0-20261016.123517-2</value>
        <updated>20261016123517</updated>
      </snapshotVersion>
      <snapshotVersion>
        <extension>jar</extension>
        <value>1.0-20261016.123517-2</value>
        <updated>20261016123517</updated>
      </snapshotVersion>
    </snapshotVersions>
  </versioning>
  <version>1.0-SNAPSHOT</version>
</metadata>

The artifact-level ("A level") metadata sits one directory higher and exists in every kind of repository. It is the version discovery file, which lists the versions of the artifact; ranges (below) are evaluated against it:

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-data</artifactId>
  <versioning>
    <versions>
      <version>1.0-SNAPSHOT</version>
    </versions>
    <lastUpdated>20261016123517</lastUpdated>
  </versioning>
</metadata>

Consuming a SNAPSHOT

The consumer tidepool-app declares the dependency io.tidepool:tidepool-data:1.0-SNAPSHOT and the file:// repository with snapshots enabled. Resolving it with an empty local repository (the output was trimmed to the lines that matter):

mvn -B dependency:list -DincludeScope=compile
[INFO] Building tidepool-app 1.0-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] Downloading from tidepool-local: file://$W/repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/maven-metadata.xml
[INFO] Downloaded from tidepool-local: file://$W/repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/maven-metadata.xml (768 B at 154 kB/s)
[INFO] Downloading from tidepool-local: file://$W/repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123517-2.pom
[INFO] Downloaded from tidepool-local: file://$W/repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123517-2.pom (940 B at 940 kB/s)
[INFO] Downloading from tidepool-local: file://$W/repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123517-2.jar
[INFO] Downloaded from tidepool-local: file://$W/repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123517-2.jar (2.1 kB at 2.1 MB/s)
[INFO] --- dependency:3.11.0:list (default-cli) @ tidepool-app ---
[INFO] The following files have been resolved:
[INFO]    io.tidepool:tidepool-data:jar:1.0-SNAPSHOT:compile -- module tidepool.data (auto)

The order of the requests is the whole mechanism. Maven asked for the version-level maven-metadata.xml first, read from it that 1.0-SNAPSHOT currently means 1.0-20261016.123517-2, and only then downloaded that POM and that JAR. Everywhere in the build, the dependency is still called 1.0-SNAPSHOT: dependency:list prints the baseVersion, not the timestamp.

sequenceDiagram participant B as Build participant L as Local repository participant R as Remote repository B->>L: need tidepool-data 1.0-SNAPSHOT L-->>B: is the cached metadata fresh enough per updatePolicy? B->>R: GET 1.0-SNAPSHOT/maven-metadata.xml R-->>B: timestamp 20261016.123517, build 2 B->>R: GET tidepool-data-1.0-20261016.123517-2.jar R-->>B: jar B->>L: store the timestamped files and a copy named 1.0-SNAPSHOT

The local repository then holds, next to the timestamped files, copies named tidepool-data-1.0-SNAPSHOT.jar and .pom, and the bookkeeping files of the resolver. The build classpath uses the 1.0-SNAPSHOT copy:

io/tidepool/tidepool-data/1.0-SNAPSHOT/_remote.repositories
io/tidepool/tidepool-data/1.0-SNAPSHOT/maven-metadata-tidepool-local.xml
io/tidepool/tidepool-data/1.0-SNAPSHOT/resolver-status.properties
io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123517-2.jar
io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123517-2.pom
io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-SNAPSHOT.jar
io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-SNAPSHOT.pom

(The .sha1 files were left out.) maven-metadata-tidepool-local.xml is the cached metadata, named after the repository id. _remote.repositories records which repository each file came from, and resolver-status.properties records when the metadata was last checked. Their format is an internal detail of the Maven Resolver and can change. After a few updates the directory holds one pair of files for every build number that was ever downloaded; Maven does not clean old ones (see the listing in the next section), which is one reason to purge a local repository from time to time with dependency:purge-local-repository.

Update policies and the -U flag

A snapshot is a moving target, so the question is how often Maven looks for a newer build. That is the update policy of a repository, set separately for <releases> and <snapshots>. The values are always, daily (the default), interval:X (where X is a number of minutes) and never, which downloads only if the file does not yet exist in the local repository. It applies to both artifacts and metadata. The command line overrides it: -U (--update-snapshots) "forces a check for missing releases and updated snapshots on remote repositories", whatever the policy says.

The experiment used the repository entry of the consumer POM, the same snapshot-enabled tidepool-local as before, and changed updatePolicy between runs. Between the runs new builds of tidepool-data were deployed (the consumer’s local repository kept the older ones). The count is the number of download lines for tidepool-local in the log:

Policy and flag Downloads What happened

daily, no -U

0

Metadata had been fetched earlier that day, so a newly deployed build 4 was not noticed.

always, no -U

3

Metadata, POM and JAR of build 4 were downloaded on the next mvn.

never, no -U, build 5 deployed

0

Nothing was checked, so build 5 was not seen.

never, with -U

3

-U overrode the policy and build 5 was downloaded.

The log of the always run, trimmed to the lines for tidepool-local:

[INFO] Downloaded from tidepool-local: file://$W/repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/maven-metadata.xml (768 B at 154 kB/s)
[INFO] Downloaded from tidepool-local: file://$W/repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123559-4.pom (940 B at 470 kB/s)
[INFO] Downloaded from tidepool-local: file://$W/repo/io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-20261016.123559-4.jar (2.1 kB at 1.0 MB/s)

After those runs the consumer’s local repository held every build that had been downloaded, plus the copy that is used:

tidepool-data-1.0-20261016.123517-2.jar
tidepool-data-1.0-20261016.123517-2.pom
tidepool-data-1.0-20261016.123544-3.jar
tidepool-data-1.0-20261016.123544-3.pom
tidepool-data-1.0-20261016.123559-4.jar
tidepool-data-1.0-20261016.123559-4.pom
tidepool-data-1.0-20261016.123607-5.jar
tidepool-data-1.0-20261016.123607-5.pom
tidepool-data-1.0-SNAPSHOT.jar
tidepool-data-1.0-SNAPSHOT.pom

Maven 3.9.16 and Maven 4.0.0-rc-7, each with an empty local repository, downloaded the same metadata and build 5 and printed the same resolved dependency. The practical rules that follow:

  • daily is the default, so two builds on the same day can use different snapshots of the same library, depending on which of them ran first on that machine. Use -U after you know a snapshot was redeployed, and in CI.

  • The policy is a property of the repository entry, not of the dependency. It is read from the <repository> in the POM or from a repository declared in a profile of settings.xml.

Enabling snapshots on a repository

Maven Central does not serve snapshots, and the super POM declares it with snapshots disabled. The effective POM of a project with no repositories of its own (mvn help:effective-pom, filtered to the repositories element) says so:

  <repositories>
    <repository>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
      <id>central</id>
      <name>Central Repository</name>
      <url>https://repo.maven.apache.org/maven2</url>
    </repository>
  </repositories>

If the only repository that can serve a SNAPSHOT has <snapshots><enabled>false</enabled></snapshots>, Maven does not try it. The consumer with that setting failed to resolve tidepool-data:1.0-SNAPSHOT; the error text printed was (the rest of the error is Maven’s generic help text):

[ERROR] Failed to execute goal on project tidepool-app: Could not resolve dependencies for project io.tidepool:tidepool-app:jar:1.0-SNAPSHOT
[ERROR] dependency: io.tidepool:tidepool-data:jar:1.0-SNAPSHOT (compile)
[ERROR] 	prefixes: Path io/tidepool/tidepool-data/1.0-SNAPSHOT/tidepool-data-1.0-SNAPSHOT.jar NOT allowed from central

The fix is to enable snapshots on the repository that hosts them. The POM can do it, as above, but the usual place is a profile in settings.xml, so that the POM does not name internal servers. That variant was run with mvn -s settings-snap.xml on a consumer POM with no <repositories> element, and resolved the dependency:

<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
  <profiles>
    <profile>
      <id>tidepool</id>
      <repositories>
        <repository>
          <id>tidepool-local</id>
          <url>file:///path/to/workspace/repo</url>
          <releases><enabled>true</enabled></releases>
          <snapshots>
            <enabled>true</enabled>
            <updatePolicy>interval:60</updatePolicy>
          </snapshots>
        </repository>
      </repositories>
    </profile>
  </profiles>
  <activeProfiles>
    <activeProfile>tidepool</activeProfile>
  </activeProfiles>
</settings>

interval:60 means "check at most once an hour". More on repositories, mirrors and the repository-manager that normally fronts them is on Repositories, Mirrors & Repository Managers, and the file itself on settings.xml & Password Encryption. The same repository entry is read from <pluginRepositories> when the SNAPSHOT is a plugin.

Locking a snapshot and keeping snapshots out of releases

You can pin a snapshot to one build by writing the timestamped version in the dependency:

<dependency>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-data</artifactId>
  <version>1.0-20261016.123544-3</version>
</dependency>

This resolved against the repository above and downloaded build 3 exactly. It is a lock-down of the bytes, not a promotion to a release: dependency:list still printed io.tidepool:tidepool-data:jar:1.0-SNAPSHOT:compile, and the enforcer’s requireReleaseDeps rule still rejected the project. The same rule on the normal 1.0-SNAPSHOT declaration (with -Denforcer.rules=requireReleaseDeps; the older -Drules still works but prints a deprecation warning):

mvn -B org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce -Denforcer.rules=requireReleaseDeps
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce (default-cli) on project tidepool-app:
[ERROR] Rule 0: org.apache.maven.enforcer.rules.dependency.RequireReleaseDeps failed with message:
[ERROR] io.tidepool:tidepool-app:jar:1.0-SNAPSHOT
[ERROR]    io.tidepool:tidepool-data:jar:1.0-SNAPSHOT <--- is not a release dependency

The pinned timestamped version failed the same rule. The Maven Release Plugin makes the same check for you before it tags a release (that plugin was not run here; see Releasing). Only rarely is a lock-down the right answer: the repository may delete old snapshot builds, and a timestamped version in a POM is still a SNAPSHOT for every tool that matters. The rest of the toolbox for this is the Enforcer.

Version ranges

A <version> element in a dependency is a version requirement, not necessarily a single version. It is soft when it is a plain version, and hard when it is written with brackets. A soft requirement is a recommendation: Maven uses it if nothing else in the graph asks for a different version nearer to the root. A hard requirement must be honoured; Maven picks the highest version of the artifact that satisfies every hard requirement, and the build fails if no version does.

Requirement Meaning

1.0

Soft requirement for 1.0. Used if no other version appears earlier in the dependency tree.

[1.0]

Hard requirement for 1.0 and only 1.0.

(,1.0]

Hard requirement for any version less than or equal to 1.0.

[1.2,1.3]

Hard requirement for any version between 1.2 and 1.3 inclusive.

[1.0,2.0)

1.0 less than or equal to x, and x less than 2.0.

[1.5,)

Hard requirement for any version greater than or equal to 1.5.

(,1.0],[1.2,)

Less than or equal to 1.0, or greater than or equal to 1.2, but not 1.1. Multiple requirements are separated by commas.

(,1.1),(1.1,)

Any version except 1.1, for example because 1.1 has a critical vulnerability.

Square brackets are inclusive, parentheses exclusive. The tidepool-units library was deployed as 1.0, 1.1, 1.2, 2.0, 2.1-beta-1, then 1.3, then 1.0.1 (that odd order matters later). A consumer whose dependency version was the property ${range} was resolved for each requirement with this small script, which ran mvn -B -U -Drange="$r" dependency:list (-U so that the metadata is always fresh) and kept the resolved version:

#!/bin/sh
for r in "$@"; do
  v=$(mvn -B -U -Drange="$r" dependency:list | sed -n 's/.*tidepool-units:jar:\([^:]*\):.*/\1/p')
  printf '%-18s %s\n' "$r" "${v:-no version}"
done
1.1                1.1
[1.0,2.0)          1.3
[1.0,2.0]          2.0
[1.1]              1.1
(,1.2]             1.2
[1.1,)             2.1-beta-1
(,1.0),(1.0,)      2.1-beta-1
[3.0,)             no version

The first row is a soft requirement and so behaves like any fixed version. For [3.0,) the script printed nothing because the build failed; the error was

[ERROR] org.eclipse.aether.resolution.VersionRangeResolutionException: No versions available for io.tidepool:tidepool-units:jar:[3.0,) within specified range

Two results deserve a second look. [1.1,) selected 2.1-beta-1, a pre-release, because an open-ended range takes the highest version in the repository, and by the order rules below 2.1-beta-1 is greater than 2.0. And [1.0,2.0) selected 1.3, even though 1.3 was deployed after 2.0, because the choice is by version order, not by deployment order. The script is this page’s own lab setup, not an example from the Maven documentation.

The range is evaluated against the metadata, and the metadata is cached

Ranges are resolved from the artifact-level metadata of every repository (the versions list shown earlier), and that metadata is cached under the update policy. The test: after the table above (so metadata was cached today), a new release 1.3 was deployed to the repository and the range [1.0,2.0) resolved again against the same local repository, first without -U, then with:

no -U:
[INFO]    io.tidepool:tidepool-units:jar:1.2:compile -- module tidepool.units (auto)
-U:
[INFO] Downloading from tidepool-local: file://$W/repo/io/tidepool/tidepool-units/maven-metadata.xml
[INFO] Downloading from tidepool-local: file://$W/repo/io/tidepool/tidepool-units/1.3/tidepool-units-1.3.pom
[INFO] Downloading from tidepool-local: file://$W/repo/io/tidepool/tidepool-units/1.3/tidepool-units-1.3.jar
[INFO]    io.tidepool:tidepool-units:jar:1.3:compile -- module tidepool.units (auto)

The same pom.xml produced 1.2 in the morning and 1.3 after lunch with no change in the project. Nothing in the repository’s state, the local repository’s age or the time of day is in the file, which is the heart of the reproducibility problem discussed below.

Ranges and mediation

A hard requirement overrides a soft one anywhere in the tree. A second library, tidepool-tides:1.0, depends on tidepool-units with the hard range [1.1,1.2]. A project that depends on both, with its own requirement for tidepool-units varied, was resolved with dependency:tree, filtered here to the lines with io.tidepool:

Direct requirement Result

1.0 (soft)

tidepool-units 1.2. The transitive hard range won over the direct soft version, and the tree listed tidepool-units first and tidepool-tides second.

[1.0,1.1]

tidepool-units 1.1. The highest version in the intersection of [1.0,1.1] and [1.1,1.2].

[1.3,)

The build failed: Unsolvable hard constraint combination; Could not resolve version conflict among [...].

[2.0]

The same failure, because 2.0 is not inside [1.1,1.2].

The complete message for the third row was

[ERROR] Unsolvable hard constraint combination; Could not resolve version conflict among [io.tidepool:tidepool-units:jar:[1.3,), io.tidepool:tidepool-tides:jar:1.0 -> io.tidepool:tidepool-units:jar:[1.1,1.2]]

The usual rule "the nearest declaration wins" is therefore suspended for hard ranges; the rule is described in Dependencies, Scopes & Mediation. A range in a library you publish is a hard requirement on all of its consumers, and consumers cannot override it with a plain version (only with an exclusion, or a range of their own that happens to fit).

Pre-releases and SNAPSHOTs inside a range

Ranges are inclusive of anything that sorts inside them, and the order rules put pre-releases below the release. So [1.0,2.0) accepts 2.0-alpha-1 and 2.0-SNAPSHOT, which both come before 2.0. The library tidepool-gauge was deployed as 1.0, 1.5, 2.0-alpha-1 and 2.0-SNAPSHOT (in that order), and resolved with the same script (the artifact id changed):

[1.0,2.0)            2.0-SNAPSHOT
[1.0,2.0-alpha-1)    1.5
[1.0,2.0-alpha)      1.5
[1.0,2.0-SNAPSHOT)   2.0-alpha-1

The first row is a trap: a range that looks like "everything before 2.0, so 1.x" selected a snapshot of the next major version, because the repository serves snapshots and the metadata listed 2.0-SNAPSHOT. The robust way to say "before any 2.0" is to make the upper bound the lowest 2.0 qualifier, 2.0-alpha, which is what the third row does. Remember that this works only if the version scheme of the library is orderly; with rc and beta qualifiers see the order table in the next section.

RELEASE and LATEST

Two special words can stand in for a version. They come from the artifact-level metadata and not from any version comparison:

  • RELEASE is the value of <release> in the metadata: the last deployed non-snapshot version.

  • LATEST is the value of <latest> when the file has one. The metadata below has no latest element, and LATEST still resolved to the same version as RELEASE.

The metadata of tidepool-units after the 1.0.1 deployment was

    <release>1.0.1</release>
      <version>1.0</version>
      <version>1.1</version>
      <version>1.2</version>
      <version>2.0</version>
      <version>2.1-beta-1</version>
      <version>1.3</version>
      <version>1.0.1</version>

(These are the release element and the versions list of the file, which is in deployment order and not sorted.) The same script with RELEASE, LATEST and [1.0,):

RELEASE   1.0.1
LATEST    1.0.1
[1.0,)    2.1-beta-1

So RELEASE returned 1.0.1, a version lower than the 2.0 already in the repository, merely because it was deployed last (a back-port to an old line, for instance, does exactly that). Maven 3.10.0, Maven 3.9.16 and Maven 4.0.0-rc-7 all resolved RELEASE and LATEST to the same version on this metadata (Maven 3.9.16 also printed an extra message about the version being cached from a repository id that is unavailable in the current build context, and then resolved it). Maven 3.10.0 and Maven 4.0.0-rc-7 printed no warning for them in a dependency. They should not be used in dependencies or plugin versions: they are even less deterministic than ranges, and plugin versions are best pinned (see Plugins & Their Configuration).

The version order

Every comparison above, from "highest version in a range" to "which snapshot is newer", uses one algorithm, implemented by the class org.apache.maven.artifact.versioning.ComparableVersion in the maven-artifact library. It is not Semantic Versioning: Maven compares versions as sequences of tokens, and it pays no attention to semantic versioning 2.0.0 ideas such as the plus sign or build identifiers. If your versions follow Semantic Versioning 1.0.0 and use lower-case letters, the result usually agrees with the semver precedence rules. The rules, summarised from the specification:

  1. Split. The version is split at dots, hyphens, underscores and at every switch between digits and letters. The separator is remembered. Empty tokens become 0. For example 1-1.foo-bar1baz-.1 becomes 1-1.foo-bar-1-baz-0.1.

  2. Trim. Trailing "null" tokens (0, the empty string, final, ga) are removed, from the end, and again at every remaining hyphen. So 1.0.0, 1.0, 1.final and 1.ga all trim to 1.

  3. Pad. The shorter sequence is padded with null tokens so that the two have the same length.

  4. Compare token by token from the left. Numeric tokens compare as integers. A qualifier is lower-cased, and the following qualifiers come first, in this order: alpha < beta < milestone < rc = cr < snapshot < "empty" = final = ga = release < sp. The letters a, b and m count as alpha, beta and milestone when directly followed by a number. Qualifiers that are not in the list sort alphabetically, after sp and before any number that follows a hyphen. Separators order as .qualifier = -qualifier < -number < .number.

The class can be called directly to check a version list. The Maven 3.10.0 lib/maven-artifact-3.10.0.jar was put on the class path of a short program that sorts its arguments with Collections.sort and prints them, with = between versions that compare equal and < otherwise:

import org.apache.maven.artifact.versioning.ComparableVersion;
import java.util.*;

public class Order {
    public static void main(String[] args) {
        List<ComparableVersion> versions = new ArrayList<>();
        for (String a : args) {
            versions.add(new ComparableVersion(a));
        }
        Collections.sort(versions);
        StringBuilder out = new StringBuilder(versions.get(0).toString());
        for (int i = 1; i < versions.size(); i++) {
            int c = versions.get(i - 1).compareTo(versions.get(i));
            out.append(c == 0 ? " = " : " < ").append(versions.get(i));
        }
        System.out.println(out);
    }
}
javac -cp apache-maven-3.10.0/lib/maven-artifact-3.10.0.jar Order.java
java -cp apache-maven-3.10.0/lib/maven-artifact-3.10.0.jar:. Order 1.10 1.9 1.0 1.0.0 1.0-final 1.0-ga \
  1.0-SNAPSHOT 1.0-alpha-1 1.0-beta-2 1.0-rc1 1.0-RC1 1.0-cr1 1.0-sp1 1.0-1 1.0.1 1.0-foo 1.0-m1 1.0-a1 2.0
1.0-alpha-1 = 1.0-a1 < 1.0-beta-2 < 1.0-m1 < 1.0-rc1 = 1.0-RC1 = 1.0-cr1 < 1.0-SNAPSHOT < 1.0 = 1.0.0 = 1.0-final = 1.0-ga < 1.0-sp1 < 1.0-foo < 1.0-1 < 1.0.1 < 1.9 < 1.10 < 2.0

Notice: 1.9 < 1.10 (numbers, not strings), 1.0-SNAPSHOT is below 1.0, 1.0-sp1 is above it, an unknown qualifier (1.0-foo) is above the service pack, and a number after a hyphen (1.0-1) is above an unknown qualifier. Case does not matter (rc1 = RC1). The same list compared with the maven-artifact jar of Maven 3.9.16 and of Maven 4.0.0-rc-7 gave an identical output (checked with md5 of the output, with three extra arguments, 1.0-20261016.123517-2, 1.0.RC1 and 1.0+build5). The program is this page’s own lab tool, not an example from the Maven documentation.

Two further results from the program, using longer lists. The first shows how pre-releases, a service pack and a timestamped snapshot version line up; the second how the choice of separator matters.

java -cp apache-maven-3.10.0/lib/maven-artifact-3.10.0.jar:. Order 1.0-alpha1 1.0-beta1 1.0-rc1 1.0 1.0-sp1 1.0.1 1.0-SNAPSHOT 1.0-20261016.123517-2
java -cp apache-maven-3.10.0/lib/maven-artifact-3.10.0.jar:. Order 1.0-RC1 1.0.RC1 1.0-rc.1 1.0.0-1 1.0.1 1.0+build5 1.0+build6 1.0-build5 1.0
1.0-alpha1 < 1.0-beta1 < 1.0-rc1 < 1.0-SNAPSHOT < 1.0 < 1.0-sp1 < 1.0-20261016.123517-2 < 1.0.1
1.0-RC1 = 1.0.RC1 < 1.0-rc.1 < 1.0 < 1.0+build5 < 1.0+build6 < 1.0-build5 < 1.0.0-1 < 1.0.1

The first line has a surprise: the timestamped version 1.0-20261016.123517-2 is greater than 1.0, 1.0-sp1 and everything else of the 1.0 line, because after the hyphen it is a number. That is exactly why Maven keeps the baseVersion (1.0-SNAPSHOT) for all the logic that needs an order, and why a timestamp must never appear in a range. On the second line a plus sign has no special meaning: 1.0+build5 and 1.0+build6 are ordered like any other qualifier text and both sort above 1.0, whereas Semantic Versioning 2.0.0 ignores build metadata when it compares.

The canonical form that ComparableVersion keeps internally can be printed with getCanonical(), which makes the trimming visible:

1.0.0              -> 1
1.0-final          -> 1
1.0.0-foo.0.0      -> 1-foo
1.0.0-beta         -> 1-beta
1.0-RC1            -> 1-rc-1
1.0.RC1            -> 1-rc-1
1.0-1              -> 1-1
1-1.foo-bar1baz-.1 -> 1-1.foo-bar-1-baz-0.1
1.0-SNAPSHOT       -> 1-snapshot

The specification recommends a few habits that avoid the traps above: use alpha, beta and milestone rather than ea or preview; write 1.0.0-RC1 rather than 1.0.0.RC1; prefer RC to the discouraged CR; do not use final, ga or release (no qualifier is the release); do not use SP (increment the patch version instead); and avoid non-ASCII characters in versions. The project-wide naming conventions are on Philosophy, Coordinates & Directory Layout.

Why ranges and snapshots hurt reproducibility

A reproducible build gives the same dependencies from the same inputs. Both features give that up:

  • A SNAPSHOT is different every time someone deploys. The same project built on Monday and Tuesday can use two different JARs of its dependency, and the POM did not change. An update policy of daily makes when the difference shows up depend on the machine.

  • A range depends on the repository’s content on the day. The [1.0,2.0) experiment above went from 1.2 to 1.3 with no edit in the consumer; the same happens when a library publishes a bad release that is in range, or a pre-release as seen with tidepool-gauge.

  • Both depend on cached metadata. Which of two machines sees the new version depends on its update policy and on the timestamp of its own resolver-status.properties.

  • A range in a library is a range in every consumer. The project that publishes [1.1,1.2] in its POM chose for all of its users, and one of them can end up with an unsolvable combination, as in the experiment above.

Practical guidance that follows from these results:

  • Use fixed release versions in dependencies, and move the choice of versions into dependencyManagement or a BOM so that it lives in one place.

  • Use SNAPSHOT dependencies only between modules and projects that you release together, never in a published library, and enforce it with requireReleaseDeps (see Locking a snapshot and keeping snapshots out of releases). Inside one multi-module build the reactor supplies the modules directly (see Multi-Module Builds & the Reactor).

  • Prefer to update versions on purpose. The MojoHaus Versions plugin (third party) lists and changes versions with goals such as versions:display-dependency-updates and versions:use-latest-releases; see Dependency Analysis & Convergence.

  • Treat a range as a deliberate and rare choice, for a library whose consumers should receive security fixes automatically, and then prefer an upper bound that excludes the next pre-release ([1.0,2.0-alpha)) over a plain 2.0.

  • For CI and releases, use -U (or an always policy for snapshots) so that two jobs do not disagree because of cached metadata, and see Security & Reproducible Builds for the rest of the reproducibility checklist.

Equivalents in Ivy and Gradle

Maven Ivy (Ant) Gradle

SNAPSHOT version, timestamped upload

A revision marked as a development integration (integration status) and latest.integration.

A changing module; the dependency cache keeps it for a default of 24 hours.

Update policy and -U

The resolver attributes checkmodified and changingPattern.

--refresh-dependencies, cacheChangingModulesFor.

Version range [1.0,2.0)

The same bracket syntax, plus a sub-revision 1.0.+ and latest.release.

The same bracket syntax, 1.+ and latest.release.

Soft requirement and the "nearest wins" rule

The conflict manager (latest-revision by default).

Highest version wins, with strict and rich version declarations.

Locking an exact resolution

Not built in; use fixed revisions.

Dependency locking and verification.

The Ivy side is on Ivy: Conflicts & Versions, the Gradle side on Platforms, Constraints & Rich Versions and Dependency Locking, Verification & Caching. The tool-neutral discussion of conflicts and ranges is on Dependency Management Fundamentals.

Summary of pitfalls

  • A SNAPSHOT is stored as 1.0-YYYYMMDD.HHMMSS-N; the baseVersion 1.0-SNAPSHOT is what the POM and dependency:list show.

  • daily is the default snapshot update policy; a build can miss a snapshot deployed an hour ago. Use -U.

  • Central serves no snapshots; a snapshot-enabled repository must be declared in the POM or in a settings.xml profile.

  • Old timestamped builds are never removed from the local repository by Maven itself.

  • A timestamped version in a dependency is a lock-down of the bytes, not a release; requireReleaseDeps still fails it.

  • A bracketed version is a hard requirement. Two incompatible hard requirements fail the build, and a hard range beats a nearer soft version.

  • [1.0,2.0) includes 2.0-alpha-1 and 2.0-SNAPSHOT; use [1.0,2.0-alpha) to exclude the pre-releases of 2.0.

  • RELEASE and LATEST mean "last deployed", not "highest"; both are best avoided.

  • Ranges read cached metadata, so two machines can resolve the same range to different versions on the same day.

  • Maven’s order is not Semantic Versioning 2.0.0: a plus sign has no special meaning, and a timestamped snapshot sorts above its release.

  • Maven 3.9.16, Maven 3.10.0 and Maven 4.0.0-rc-7 (a release candidate) gave the same results for snapshot consumption, the [1.0,2.0) range, RELEASE, LATEST and the version order experiments on this page.