Skip to content

Instantly share code, notes, and snippets.

@carlosame
Created July 14, 2020 13:00
Show Gist options
  • Save carlosame/1b67f7046c5ab68220b45731a2a42551 to your computer and use it in GitHub Desktop.
Save carlosame/1b67f7046c5ab68220b45731a2a42551 to your computer and use it in GitHub Desktop.
Maven dependency version ranges for libraries

Maven dependency version ranges

Introduction

Java:tm: projects generally depend on other projects, and those dependencies are detailed in the file that describes how to build the project, often the Maven POM.

The build process needs to know which version or versions of the dependencies are required to succeed, and projects often specify a single, fixed version, perhaps the same one that was used when the project's code was edited with the help of an IDE. In the Maven POM, this is done by introducing a line like:

<version>1.7.28</version>

in that dependency's section. If our project is an application, that's fine because in principle this determines which version is going to be used. However, we could find out that Maven cannot deliver the version that we requested, due to constrains that were put either deliberately or unknowingly by some of our other dependencies.

Dependency versions for a library

If our project is a software library, we could try to put a line like the above to set the dependency version(s) that we used to build and test our project.

In normal POM setups, that would not put a specific constraint on what the downstream applications (the ones that depend on our library) can do. But in downstream applications that have mission-critical setups (which use the maven-enforcer-plugin) setting a fixed version effectively introduces a lower limit on the versions that can be used downstream. That is, in that environment the above line behaves like:

<version>[1.7.28,)</version>

which is how a lower version limit can be specified in Maven. A lot of developers are unaware of that behaviour, and keep setting somewhat arbitrary fixed dependency versions (for example the latest one that they downloaded) in their libraries' POM, believing that this is harmless. But their choice could limit what downstream can do.

Reacting to the issue, some people believe that setting the version to a fixed, old dependency would solve the problem, but that approach has potential issues. First, a fixed version is often interpreted as an endorsement, a recommended version to use. Additionally, a Maven build generally involves running unit tests, and developers generally prefer to use a fairly recent version for that.

Instead, using a version range is a good solution for many cases, and is also what Oracle recommends in this Fusion manual. To see why, let's examine an hypothetical case (which summarizes actual cases) as an example.

A typical library scenario

Let's assume that our project is a library that, in turn, depends on libA which is an (invented) library. Its version landscape could be described as follows:

  • Versions 1.x are compatible with Java 7.
  • Versions 2.x are compatible with Java 8.

So some applications may want to still use 1.x for Java 7. So far, setting the dependency version as <version>1.0</version> could do the trick, but when a software project produces newer versions that's often for a reason: bugs were fixed.

Imagine the following (which reflects a real situation):

  • Version 2.1 fixes a bug or varies the behaviour in a subtle way, so one of the tests in our test battery (we are a library that depends on libA) fails.

Since libA's released 2.1, we modified the failing test so it matches what 2.1 produces. After that, our software can still compile with 1.0 or higher, but our tests need at least 2.1.

We could require 2.1 in the POM (resulting in '2.1 or higher' for the aforementioned cases), however some downstream applications may depend on Java 7 (requiring 1.x) or decide that they want the old 2.0 behaviour until they adapt their software. The following is the solution to put in that dependency's section of our library's POM:

<version>[1.0,)</version>

With the above configuration, we do not put a strong limitation on our downstream users (just '1.0 or higher'), and at the same time the latest version of libA (perhaps 2.2 now) is going to be used when we do a Maven build and run our test battery.

Downsides

While the usage of version ranges is convenient, it may introduce potential instabilities when we build our library with Maven (instead of building from the IDE), possibly for artifact deployment. If the latest resolved dependency (which may be different to the one that was used in the IDE) breaks the API or ships with a major bug, that may break the build. However, this is something that we are expected to deal with anyway, and is a small cost in exchange for the increased convenience for downstream users.

Upper bounds

Setting an upper bound to a dependency version is generally a bad idea, and should only be done if you are confident that your library would be incompatible with the excluded versions, for example:

<version>[1.0, 2)</version>

which would exclude versions 2.0 and higher. But beware that suffixes like "-RC1" or others can cause an artifact to be considered as 'lesser' than your upper bound. For example 2.0-RC1 is considered to be previous to 2.0; you can verify that for yourself using the maven-artifact tool:

$ java -jar ${MAVEN_HOME}/lib/maven-artifact-3.x.jar 2.0 2.0-RC1
Display parameters as parsed by Maven (in canonical form) and comparison result:
1. 2.0 == 2
   2.0 > 2.0-RC1
2. 2.0-RC1 == 2-rc-1
$ java -jar ${MAVEN_HOME}/lib/maven-artifact-3.x.jar 2.0 2.0-beta1
Display parameters as parsed by Maven (in canonical form) and comparison result:
1. 2.0 == 2
   2.0 > 2.0-beta1
2. 2.0-beta1 == 2-beta-1
$ java -jar ${MAVEN_HOME}/lib/maven-artifact-3.x.jar 2.0 2.0-alpha1
Display parameters as parsed by Maven (in canonical form) and comparison result:
1. 2.0 == 2
   2.0 > 2.0-alpha1
2. 2.0-alpha1 == 2-alpha-1

So instead of the previous version range, you could prefer this:

<version>[1.0,1.999)</version>

Conclusions

Version ranges can be the best (or less bad) solution for a library to specify the dependency versions that it requires. The exception to that statement could be any dependency which is not critical (so it is not important to use the latest versions in tests) or where we have doubts about future API stability; in that case it may be better to specify a fixed version for that less-known and possibly unstable dependency.

If you are not sure about what kind of version specification should you use, look at your dependencies on a case-by-case basis with potential scenarios in mind: generic advices may miss your real-world situations after all. There is no replacement for a detailed knowledge about your dependencies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment