Maven Dependency Management & BOMs
|
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, the primary runtime, with Maven 3.9.16 and Maven 4.0.0-rc-7 (a release candidate) used wherever behaviour could differ. Every example was run on JDK 26 on 2026-10-16, with the Dependency Plugin 3.11.0 (pinned for the Maven 3.9.16 and 4.0.0-rc-7 runs, because Maven 3.9.16 would otherwise use its older default). Paths in the console output were shortened and Maven’s progress lines (download messages, resources and compiler plugin lines) were left out; nothing else was edited. Anything that was not run is labelled "not run here".
A real build has dozens of dependencies and many modules. Without help, the same version number is typed in every module, one
module drifts, and the version of a library that you never declared is chosen by whichever dependency happens to come first.
Maven’s answer is dependencyManagement: one place that says which version (and scope, and exclusions) applies to a given
artifact wherever it shows up in the graph, direct or transitive. A Bill of Materials (BOM) is that same section packaged as
a POM that other projects import. How the graph is built (scopes and nearest-wins mediation) is on
Dependencies, Scopes & Mediation;
how to inspect and police it is on
Dependency Analysis & Convergence;
parent POMs and inheritance are on
Inheritance, Aggregation & the Effective POM;
version syntax is on
Versions, Snapshots & Ranges; the
cross-tool vocabulary (constraints, platforms, locking) is in
Dependency Management Fundamentals.
The problem: versions typed everywhere
The scratch tidepool build used on these pages has four modules: tidepool-model, tidepool-engine, tidepool-cli and
tidepool-report. In this first version every module spells out its own versions, exactly as a beginner would write it. The
engine declares Commons CSV 1.14.1 and the CLI, written months later, declares 1.14.0. The report module uses Commons
FileUpload 1.5 next to Commons CSV 1.14.1. The tree of the whole reactor, filtered to the three libraries involved
(mvn -Dverbose -Dincludes=commons-io,commons-codec,org.apache.commons:commons-csv compile dependency:tree), shows two silent
inconsistencies:
--- dependency:3.11.0:tree (default-cli) @ tidepool-engine ---
io.tidepool:tidepool-engine:jar:1.0.0
\- org.apache.commons:commons-csv:jar:1.14.1:compile
+- commons-io:commons-io:jar:2.20.0:compile
\- commons-codec:commons-codec:jar:1.19.0:compile
--- dependency:3.11.0:tree (default-cli) @ tidepool-cli ---
io.tidepool:tidepool-cli:jar:1.0.0
+- io.tidepool:tidepool-engine:jar:1.0.0:compile
| \- (org.apache.commons:commons-csv:jar:1.14.1:compile - omitted for conflict with 1.14.0)
\- org.apache.commons:commons-csv:jar:1.14.0:compile
+- commons-io:commons-io:jar:2.18.0:compile
\- commons-codec:commons-codec:jar:1.18.0:compile
--- dependency:3.11.0:tree (default-cli) @ tidepool-report ---
io.tidepool:tidepool-report:jar:1.0.0
+- commons-fileupload:commons-fileupload:jar:1.5:compile
| \- commons-io:commons-io:jar:2.11.0:compile
\- org.apache.commons:commons-csv:jar:1.14.1:compile
+- (commons-io:commons-io:jar:2.20.0:compile - omitted for conflict with 2.11.0)
\- commons-codec:commons-codec:jar:1.19.0:compile
(The tidepool-parent and tidepool-model sections are empty here and were omitted.) The CLI ships a CSV library older than
the engine it depends on, and therefore also Commons IO 2.18.0 and Commons Codec 1.18.0 instead of 2.20.0 and 1.19.0. In the
report module Commons IO 2.11.0 wins only because Commons FileUpload is declared first, while the library that actually
needs the newer one (Commons CSV) loses the conflict. Nobody decided any of this.
Managing versions in one place
The fix is a dependencyManagement section in the parent POM. It lists artifacts with the version (and optionally scope,
exclusions, type and classifier) that should apply; it does not add anything to any classpath.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.7</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
The modules then name only what they need. This is the whole <dependencies> block of tidepool-cli:
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
</dependencies>
A dependency is matched against the management section by the four identifying elements groupId, artifactId, type
and classifier. Type defaults to jar and the classifier to none, which is why groupId and artifactId usually
suffice; a test-jar or a war dependency needs its type repeated to find its managed entry. mvn help:effective-pom
(see
the effective POM)
shows the result of the merge for the CLI module: version and scope were filled in, including test for JUnit, which the
module did not write.
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
The same verbose tree now reads (again filtered to the three libraries):
--- dependency:3.11.0:tree (default-cli) @ tidepool-cli ---
io.tidepool:tidepool-cli:jar:1.0.0
+- io.tidepool:tidepool-engine:jar:1.0.0:compile
| \- (org.apache.commons:commons-csv:jar:1.14.1:compile - version managed from 1.14.1; omitted for duplicate)
\- org.apache.commons:commons-csv:jar:1.14.1:compile
+- commons-io:commons-io:jar:2.20.0:compile (version managed from 2.20.0)
\- commons-codec:commons-codec:jar:1.19.0:compile
--- dependency:3.11.0:tree (default-cli) @ tidepool-report ---
io.tidepool:tidepool-report:jar:1.0.0
+- commons-fileupload:commons-fileupload:jar:1.5:compile
| \- commons-io:commons-io:jar:2.20.0:compile (version managed from 2.11.0)
\- org.apache.commons:commons-csv:jar:1.14.1:compile
+- (commons-io:commons-io:jar:2.20.0:compile - version managed from 2.20.0; omitted for duplicate)
\- commons-codec:commons-codec:jar:1.19.0:compile
The note version managed from X is printed by -Dverbose whenever management is consulted for an artifact; when X is
the same version it is just noise, and when it differs (managed from 2.11.0) management has overridden what the
declaring POM asked for. In the report module, Commons FileUpload still asks for Commons IO 2.11.0 in its own POM, but the
build uses 2.20.0, regardless of declaration order. The output of this and the following examples was identical on Maven
3.9.16, Maven 3.10.0 and Maven 4.0.0-rc-7.
It controls, it does not add
Management only configures dependencies that something else brings in. The tidepool-model module declares none, and
although the parent manages six artifacts its tree is just itself:
--- dependency:3.11.0:tree (default-cli) @ tidepool-model ---
io.tidepool:tidepool-model:jar:1.0.0
A dependency that is declared with no version and is not managed fails the build. This is the message when
tidepool-report omits the version of Commons FileUpload, which nobody manages (run before the section was added; Maven
3.10.0):
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[ERROR] 'dependencies.dependency.version' for commons-fileupload:commons-fileupload:jar is missing. @ line 16, column 17
[ERROR] The build could not read 1 project -> [Help 1]
Precedence for the version of one dependency
For the version actually used, from strongest to weakest:
Four of these steps were run (all three Maven versions gave identical results). A direct declaration with an explicit
version is not overridden by management. If tidepool-cli is changed to declare Commons CSV 1.10.0 itself, the managed
1.14.1 that the engine would give it is ignored:
--- dependency:3.11.0:tree (default-cli) @ tidepool-cli ---
io.tidepool:tidepool-cli:jar:1.0.0
+- io.tidepool:tidepool-engine:jar:1.0.0:compile
| \- (org.apache.commons:commons-csv:jar:1.14.1:compile - version managed from 1.14.1; omitted for conflict with 1.10.0)
\- org.apache.commons:commons-csv:jar:1.10.0:compile
(Filtered to Commons CSV.) The declaration wins and the drift is back; dependency:analyze-dep-mgt ignores direct
dependencies by default and reported None for every module of this build, see
Dependency Analysis & Convergence
for -Dmdep.analyze.ignore.direct=false and the enforcer rules that catch it. The rule for team practice is simple: in a
multi-module build, never write a version on a dependency whose version is managed.
A child that manages the same artifact again overrides its parent. A child that imports a BOM does not: the parent’s managed entry wins over the imported one, because imports only fill gaps. Both cases were run with a parent that manages Commons IO 2.20.0:
| Child POM | Managed in child | Commons IO used |
|---|---|---|
Manages |
2.15.1 |
2.15.1 |
Imports a BOM that manages |
2.11.0 (from the BOM) |
2.20.0 (the parent wins) |
The Maven documentation states the same rule for transitive dependencies: dependency management takes precedence over mediation, so a managed version is chosen whichever dependency refers to the artifact, and the declaration in the current POM takes precedence over the one in its parent. It also states that management does not affect the dependencies of plugins in the same POM, only project dependencies; this last point was not run here.
Scope and exclusions are managed too
The scope in a management entry is applied to direct dependencies that omit it (JUnit above became test that way) and also
to transitive occurrences. Adding Commons Codec to the parent with <scope>runtime</scope> changes how it arrives through
Commons CSV:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.19.0</version>
<scope>runtime</scope>
</dependency>
--- dependency:3.11.0:tree (default-cli) @ tidepool-cli ---
io.tidepool:tidepool-cli:jar:1.0.0
\- org.apache.commons:commons-csv:jar:1.14.1:compile
+- commons-io:commons-io:jar:2.20.0:compile
\- commons-codec:commons-codec:jar:1.19.0:runtime
Commons Codec is now on the runtime classpath only, not the compile classpath of the CLI, although Commons CSV declares it as compile. Use this deliberately; changing the scope of a library behind someone else’s API can break compilation of code that uses it.
An <exclusions> element in a managed entry applies to every dependency path that reaches that artifact, so one
declaration in the parent replaces the same exclusion repeated in each module:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
<exclusions>
<exclusion>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</exclusion>
</exclusions>
</dependency>
--- dependency:3.11.0:tree (default-cli) @ tidepool-cli ---
io.tidepool:tidepool-cli:jar:1.0.0
\- org.apache.commons:commons-csv:jar:1.14.1:compile
\- commons-io:commons-io:jar:2.20.0:compile
Exclusions themselves are explained on Dependencies, Scopes & Mediation.
Source: POM Reference: Dependency Management, Introduction to the dependency mechanism: Dependency Management
Properties for versions
It is common to name versions as properties (<commons-io.version>2.20.0</commons-io.version>) and use
${commons-io.version} in the management section. With inheritance, a child can override the property to change the
managed version; this was run with a parent that manages Commons IO as ${commons-io.version} set to 2.20.0, and a child that
sets the property to 2.11.0:
--- dependency:3.11.0:tree (default-cli) @ override-parent ---
com.example:override-parent:jar:1.0.0
\- commons-io:commons-io:jar:2.11.0:compile
This only works through inheritance. See the import section below for the case where it silently does not.
Importing a BOM
A project has only one parent, so management cannot be inherited from two places. The answer is import: declare a POM as a
dependency with <type>pom</type> and <scope>import</scope> inside dependencyManagement, and its
dependencyManagement entries are merged into yours. A BOM ("Bill of Materials") is a POM written for exactly that purpose.
A consumer of the tidepool BOM (the BOM itself is built in the next section) looks like this:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>harbour-app</artifactId>
<version>1.0.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Neither dependency has a version; both come from the BOM (the engine) or from the BOM’s own import of the JUnit BOM:
--- dependency:3.11.0:tree (default-cli) @ harbour-app ---
com.example:harbour-app:jar:1.0.0
+- io.tidepool:tidepool-engine:jar:1.0.0:compile
| +- io.tidepool:tidepool-model:jar:1.0.0:compile
| \- org.apache.commons:commons-csv:jar:1.14.1:compile
| +- commons-io:commons-io:jar:2.20.0:compile
| \- commons-codec:commons-codec:jar:1.19.0:compile
\- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
+- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test
| +- org.opentest4j:opentest4j:jar:1.3.0:test
| +- org.junit.platform:junit-platform-commons:jar:6.1.3:test
| +- org.apiguardian:apiguardian-api:jar:1.1.2:test
| \- org.jspecify:jspecify:jar:1.0.0:test
+- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test
\- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test
\- org.junit.platform:junit-platform-engine:jar:6.1.3:test
The effective POM of the consumer (mvn help:effective-pom) holds 22 managed entries: the six the BOM wrote itself plus the
sixteen it received from junit-bom. Imports are recursive: a BOM may import another BOM, and the importing project sees
the union as though they had been written in the BOM.
What an import takes, and what it ignores
An import brings in only the dependencyManagement section of the imported POM (after resolving that POM’s own parent
chain and properties). Its <dependencies> are not added to the consumer. To check this a test BOM, bom-a, was given a
real <dependencies> block with Commons Codec next to its management entries; a project that imports it did not get
Commons Codec on its classpath (the trees in the next table show only the two libraries the project declared).
The rules are strict and the failures are quiet. Both mistakes below produced a warning on all three Maven versions, then
a build that succeeded without applying the import (BUILD SUCCESS on all three; Maven 3.10.0 messages shown):
[WARNING] 'dependencies.dependency.scope' for io.tidepool.demo:bom-b:pom must be one of [provided, compile, compile-only, runtime, test, system] but is 'import'. @ line 14, column 14
That one is <scope>import</scope> placed in <dependencies> instead of in dependencyManagement; the build then
treated the BOM as an ordinary dependency (io.tidepool.demo:bom-b:pom:1.0.0:import in the tree). The next one is an import
that omits <type>pom</type>:
[WARNING] 'dependencyManagement.dependencies.dependency.type' for io.tidepool.demo:bom-b:jar must be 'pom' to import the managed dependencies. @ line 10, column 19
The managed versions simply did not apply. Maven 3.9.16 prints the same two messages without compile-only in the list of
valid scopes; Maven 4.0.0-rc-7 prints a shorter summary ("1 problem was encountered … use -e to see details") and the same
detail with -e. After adding an import, always run dependency:tree or help:effective-pom and look for the versions.
Order of imports
Several BOMs can be imported, and they may disagree. The first import that manages an artifact wins; a managed entry written
in the importing POM, or inherited, beats both (the previous section). The test used two BOMs, bom-a (Commons IO 2.11.0,
picocli 4.7.6) and bom-b (Commons IO 2.20.0, picocli 4.7.7), and three projects that declare Commons IO and picocli without
versions. All three Maven versions agreed:
| Management in the project | commons-io | picocli |
|---|---|---|
imports |
2.11.0 |
4.7.6 |
imports |
2.20.0 |
4.7.7 |
imports |
2.20.0 |
4.7.5 |
Reordering two import blocks therefore changes the classpath without touching a dependency, and a library’s own override
beats every BOM wherever it is written. Treat import order as part of the contract, and use dependency:tree after a
reorder.
Properties and imports do not mix
A BOM’s ${commons-io.version} is resolved in the context of the BOM, not of the importer. A project that imports
tidepool-bom (which uses properties for its third-party versions) and sets its own commons-io.version property to 2.11.0
gets no change: the tree still shows Commons IO 2.20.0 (Maven 3.9.16, 3.10.0 and 4.0.0-rc-7 identical). Override a BOM’s
version by managing the artifact in your POM, as in the last row of the table above, not by shadowing its property.
Writing a BOM: the tidepool BOM
A BOM is an ordinary POM with <packaging>pom</packaging> whose only job is a dependencyManagement section. The tidepool
one manages the three library modules at its own version, so that all of them are always consumed at matching versions, plus
the third-party libraries the project has tested against, and imports JUnit’s own BOM for the test side:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-bom</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<name>Tidepool BOM</name>
<description>Versions of the Tidepool modules and of the libraries they are tested with.</description>
<properties>
<commons-csv.version>1.14.1</commons-csv.version>
<commons-io.version>2.20.0</commons-io.version>
<picocli.version>4.7.7</picocli.version>
<junit.version>6.1.3</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- the library's own modules, all at the version of this BOM -->
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-cli</artifactId>
<version>${project.version}</version>
</dependency>
<!-- third-party libraries -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>${commons-csv.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>${picocli.version}</version>
</dependency>
<!-- a BOM can import another BOM -->
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Guidelines that follow from the mechanics shown above:
-
No
<dependencies>. They would not be inherited by importers, so they mislead readers. Likewise no plugins and no build configuration: that belongs to a parent POM, which a BOM should not need to have. -
No
<parent>unless it is needed, and no scopes or exclusions unless they are universally correct. Importers cannot remove an entry; they can only override it. -
One BOM version per release of the library, with
${project.version}for its own modules, so a consumer who upgrades the BOM upgrades every module together. -
A property per third-party version is convenient inside the BOM; remember importers cannot override it (the previous section).
-
Import other BOMs for families of artifacts (here JUnit) rather than copying their lists.
The BOM was built and installed into a scratch local repository with mvn install (Maven 3.10.0) before the consumer above
resolved it. Publishing to a remote repository (mvn deploy with distributionManagement and credentials, see
Install, Deploy & Distribution Management)
is the same for a BOM as for any other POM artifact and was not run here.
BOM or parent POM
| Parent POM | BOM | |
|---|---|---|
How a project uses it |
|
|
What the project receives |
Everything: properties, |
Only the managed dependencies |
Typical owner |
The organisation or the multi-module build |
A library or framework that wants consumers to stay in sync |
Overrides |
Child manages again, or sets a property the parent uses |
The importer manages the artifact itself; properties of the BOM cannot be overridden |
A multi-module build commonly has both: a parent for its own modules, and imports of third-party BOMs inside the parent’s
dependencyManagement. The parent POM itself and the effective-POM merge are on
Inheritance, Aggregation & the Effective POM.
Maven 4: the bom packaging, the consumer POM and classified BOMs
Maven 4 (4.0.0-rc-7 here) introduces a dedicated bom packaging so that a BOM is no longer a pom that happens to be used
as one. The new packaging needs model version 4.1.0, which only Maven 4 reads, but Maven 4 writes a Maven 3 compatible
consumer POM for the repository. The build POM:
<project xmlns="http://maven.apache.org/POM/4.1.0">
<modelVersion>4.1.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-bom4</artifactId>
<version>1.0.0</version>
<packaging>bom</packaging>
<name>Tidepool BOM (Maven 4 packaging)</name>
<properties>
<commons-csv.version>1.14.1</commons-csv.version>
<commons-io.version>2.20.0</commons-io.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>${commons-csv.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
mvn install with Maven 4.0.0-rc-7 prints that the packaging is changed to pom and installs two POM files into the
repository, the original with a -build suffix and the generated consumer POM under the normal name (paths shortened):
[INFO] The packaging attribute of the 'Tidepool BOM (Maven 4 packaging)' project is configured as 'bom' and changed to 'pom'
[WARNING] Version not locked for default bindings plugins [maven-install-plugin], you should define versions in pluginManagement section of your pom.xml or parent
[INFO] --- install:3.2.0:install (default-install) @ tidepool-bom4 ---
[INFO] Installing .../bom4/pom.xml to .../repo/io/tidepool/tidepool-bom4/1.0.0/tidepool-bom4-1.0.0-build.pom
[INFO] Installing .../bom4/target/consumer-6456160834115253385.pom to .../repo/io/tidepool/tidepool-bom4/1.0.0/tidepool-bom4-1.0.0.pom
The consumer POM that is stored under tidepool-bom4-1.0.0.pom is plain Maven 3 model 4.0.0: packaging pom, the property
references already replaced by the values, and the properties section gone:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-bom4</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<name>Tidepool BOM (Maven 4 packaging)</name>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Consequently every Maven version can consume it. A project like harbour-app above, importing tidepool-bom4 and declaring
tidepool-engine without a version, produced the same tree on Maven 3.9.16, 3.10.0 and 4.0.0-rc-7: the engine, the model,
Commons CSV 1.14.1, Commons IO 2.20.0 and Commons Codec 1.19.0. What 3.9.16 and 3.10.0 cannot do is build the BOM project
itself. Maven 3.10.0 on the build POM:
[ERROR] 'modelVersion' of '4.1.0' is newer than the versions supported by this version of Maven: [4.0.0]. Building this project requires a newer version of Maven. @ line 5, column 17
The project that produces the BOM therefore needs Maven 4; the projects that import it do not.
The Maven 4 documentation also describes a consumer POM that is flattened (no parent reference, BOM imports expanded,
only used managed dependencies kept) when the user property maven.consumer.pom.flatten is true, and it is off by
default. That flattening was not run here; the consumer POM above is the default output.
Do not import a BOM from the same reactor
Maven 4 recommends that the BOM an application imports is built and available before the application is, not part of the
same reactor. A reactor (model version 4.1.0) holding a bom project and an application that imports it by its coordinates
prints, with Maven 4.0.0-rc-7 and mvn validate:
[WARNING] BOM imports from within reactor should be avoided @ line 13, column 7
The same reactor written with model version 4.0.0 and a plain pom packaging built silently on Maven 3.9.16, 3.10.0 and
4.0.0-rc-7, so the warning belongs to the new packaging. The Maven documentation says that it may become a build failure in
the future. This is the reason the tidepool
BOM above is a standalone project built and released before its consumers, not a module of the multi-module build.
Classified BOMs
In model version 4.1.0 an import dependency may carry a <classifier>, so that a project can publish its BOM as an
attached artifact next to its normal POM (for example tidepool-bom-1.0.0-bom.pom). Maven 4.0.0-rc-7 resolved this
import (a 4.1.0 build POM that sets <classifier>bom</classifier> next to <type>pom</type> and <scope>import</scope>) and
produced the expected tree for Commons CSV, Commons IO 2.20.0 and Commons Codec 1.19.0. No BOM-building plugin was used: the
classified file was put into the scratch repository with install:install-file -Dclassifier=bom -Dpackaging=pom, so
producing the classified artifact with a real plugin and deploying it was not run here.
Maven 3 rejects the same import, first because it does not know model version 4.1.0 and then because it does not allow a classifier on an import. Maven 3.10.0 and 3.9.16 both printed:
[ERROR] 'dependencyManagement.dependencies.dependency.classifier' for io.tidepool:tidepool-bom:pom:bom must be empty, imported POM cannot have a classifier. @ line 14, column 21
Both 3.x versions also printed the model version error ("has 2 errors"). The documentation adds that Maven 4 can exclude
dependencies that a BOM declares by using the existing <exclusions> element on the import; this was not run here.
Differences between Maven versions
Everything shown above ran on three Maven versions; the table lists every difference that was observed.
| Observation | Maven 3.9.16 | Maven 3.10.0 | Maven 4.0.0-rc-7 |
|---|---|---|---|
Version, scope and exclusions managed; managed beats mediation; import order; local over parent over import |
identical |
identical |
identical |
Properties of an imported BOM cannot be overridden by the importer |
identical |
identical |
identical |
Warning for |
valid scopes listed without |
valid scopes list includes |
short summary, detail with |
|
cannot build the project |
cannot build the project |
builds; installs a consumer POM and the build POM |
Importing a |
cannot build the 4.1.0 project |
cannot build the 4.1.0 project |
|
Import with |
error (and the model version error) |
error (and the model version error) |
resolved (4.1.0 build POM) |
Maven 3.9.16 uses an older default Dependency Plugin (3.7.0) than the 3.11.0 that was pinned for the comparison runs; its tree lines were identical for the examples here.
Summary of pitfalls
-
Management configures, it does not add. A dependency with no version that nobody manages is an error.
-
A direct declaration with its own version beats management; remove versions of managed artifacts from modules.
-
Management beats mediation for transitive dependencies: the managed version replaces what the declaring POM asked for, so a library can end up on a version it was never tested against.
-
A managed scope also changes the scope of transitive occurrences.
-
scopeimportworks only independencyManagementwith<type>pom</type>; the wrong forms warn and are ignored. -
An imported BOM contributes only its
dependencyManagement; its dependencies, plugins and properties are not inherited, and its own properties cannot be overridden by the importer. -
The first import wins; a local or inherited entry beats every import. Reordering imports changes the classpath.
-
A BOM build needs Maven 4 only when it uses the
bompackaging; its consumers do not. -
Keep a BOM out of the reactor that consumes it (Maven 4 warns).
-
Always confirm the outcome with
dependency:tree -Dverboseorhelp:effective-pom.