Understanding Dependency Management in Maven - Part 2

Sonam Shenoy
10 min readJul 22, 2023

--

This blog is a continuation of Understanding Dependency Management in Maven — Part 1. Do go through it before proceeding to read this blog for prior knowledge! :)

The <dependencyManagement> tag

Remember we had asked in the beginning of the previous blog — how adding a dependency under the <dependencies> tag worked even without specifying the version? Can we do without the version, though the Maven documentation says otherwise?

Nope, that’s not possible. If the version hasn’t been specified in the <dependencies> tag, it has been elsewhere. And therefore, we bring in another new tag — <dependencyManagement>.

It is similar to menus in a restaurant where the ingredients are listed below each dish (okay, not all menus mention the ingredients; most don’t, but we’re talking about the ones that do). In this tag, you define, that if you are using the dependencyX, use versionY of that dependencyX, as shown below:

<dependencyManagement>
<dependencies>
<dependency>
<groupId>groupIdOfDependencyX</groupId>
<artifactId>artifactIdOfDependencyX</artifactId>
<version>versionYofDependencyX</version>
</dependency>
</dependencies>
</dependencyManagement>

But remember that adding a dependency here, doesn’t include that dependency into your project. The listing of food items with ingredients in a menu doesn’t imply that a customer is served the entire menu. Its only purpose is to serve as a reference with regard to the ingredients that will be used in a dish, “if” the customer chooses to order it. When the customer does place an order for certain dishes, it’s then that those dishes will be cooked with the very ingredients that were stated under it in the menu.

Similarly, when you add a dependency to the <dependencies> tag without declaring the version, you will get the very version of the dependency which has been associated with that dependency under the <dependencyManagement> tag that we talked of above. Thus through <dependencyManagement>, you are instructing Maven to use the stated version of a dependency, “in case” the user decides to use that dependency.

But if the version of a library has not been defined in the <dependencyManagement> tag, and you decide to use that library, you will need to mention the version of that library yourself directly under <dependencies>, without which the build will fail. You can also take advantage of this fact to override the version of a certain dependency configured under <dependencyManagement> by specifying your required version of that dependency directly under <dependencies>.

On a side note, not just the versions of dependencies, but even other configurations that can be added under <dependencies>, such as the <exclusions> cascade from dependencyManagement to dependencies; which means that, if you have excluded a library from a dependency in the <dependencyManagement> section, you don’t have to exclude the same library from that dependency again when listing it under <dependencies>.

So what’s the purpose of the <dependencyManagement> tag when you can those very things directly under the <dependencies> tag. Simple. To avoid repetitions of dependency configurations and to centralize them. You don’t have to update the version of a dependency in multiple places (modules) every time it needs to be changed. This will be clearer once you go through the next section — BOM.

BOM

POM is now massive. It has the menu, the ingredients, the orders placed, the food — and it has become a big mess of a thing. That being the case, let’s move an entire section out of the POM, into the BOM.

What is the BOM? The name is self-evident — it is just a ‘Bill Of Materials’. Which section did we speak of moving out of the POM into the BOM? The <dependencyManagement>! That simple. It is just a POM, but with only that section. In simple words, BOM is a name given to a POM with just the <dependencyManagement> section. Thus, just like any other POM, it is and independent file of its own, and is identified via its own groupId, artifactId and version. This way, now our POM is much cleaner.

We moved our entire <dependencyManagement> section into an entirely different file — the bom.xml a.k.a. the BOM. Now how do we tell our POM to use the <dependencyManagement> section of this BOM? Just like you would add any other dependency, add this to your POM:

<dependencyManagement>
<dependencies>
<dependency>
<groupId>theGroupIdOfTheBOM</groupId>
<artifactId>theArtifactIdOfTheBOM</artifactId>
<version>theVersionOfTheBOMYouWantToUse</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

The most important lines in the code snippet listed above are the last two — the scope and type. As has been explained very well in the Maven documentation, the scope import indicates that the entire dependency on BOM shown in the snippet above, is to be replaced with the list of dependencies listed under the <dependencyManagement> tag of the imported POM (in our case, the BOM). Note that the above lines of code have themselves been added under the <dependencyManagement> section of the POM.

‘Maven scopes’ is a completely different concept and has been skipped in this blog in order to prevent this blog from growing in size. I will try to create a separate blog on this.

The type just specifies the packaging of the dependency. Here, we are importing the BOM dependency — which is of the type POM. Thus, in the BOM, you would be defining the packaging as pom, unlike the usual packaging of jar/war.

<packaging>pom</packaging>

What’s the other advantage of a BOM? This version management can now be used by more than just one project.

The BOM does absolutely nothing else. Only those dependencies are downloaded that are mentioned in your project’s POM (and its parent POM) under <dependencies>. The BOM (and the parent POM too ideally) are mainly used for their <dependencyManagement> sections; thus these files ideally don’t have the <dependencies> section. Which is why even if a dependency is mentioned in a BOM under <dependencyManagement>, you’ll have to specify them in your own project again directly under <dependencies> in order to actually use it. BOM just helps manage versions of dependencies “if” you choose to use those dependencies.

Now what is a parent POM? More on this in the next section.

But hold on! Including a dependency worked for you without specifying the version. However, you are not able to find the version of that library in the <dependencyManagement> section of your POM, or the BOM that your POM imported either. So, does it mean that it actually can work without specifying the version? Once more, No! If not here, the version would have been specified elsewhere! Where, this time? Inside the BOM that the BOM we imported into our project, might itself be importing. Yes, the chain doesn’t end at one level. The BOM that we use (the one that our POM imported), might itself be using (importing) another BOM in order to maintain versions of a certain set of libraries. This is very common.

And thus, go up up up the ladder, and you’ll eventually find what you’re searching for — the version of the library you imported, in one of the BOMs/POMs. Hunt for it!

Parent POM

Are BOM and a parent POM the same?

Firstly, let’s understand what a parent POM is, and then compare it to a BOM.

Suppose you have a multi-module Maven project, and you have several things in common to the POMs of all the modules. This might include not just the dependencies, but several other sections of the POM, such as the properties, the plugin configuration and the repositories specification. Instead of having to repeat these configurations in the POMs of each of these modules, create a parent POM of all of them. And move the configurations common to all those modules into this one parent POM of theirs. Another benefit of the parent POM is that, running a maven command on the parent POM, runs the command on all its child modules too. How to create a parent POM? Just 3 steps:

Create a pom.xml at the very root of your multi-module project. This will be the parent POM of all the modules of that project. And in this pom.xml:

1. Add the packaging type as ‘pom’.

2. Under the <modules> tag, list all the child modules of the multi-module project.

<packaging>pom</packaging>
<modules>
<module>childModuleA</module>
<module>childModuleB</module>
</modules>

In the pom.xml of each of the child modules:

1. Specify their <parent> as the parent POM we created above:

<parent>
<groupId>parentPOMsGroupId</groupId>
<artifactId>parentPOMsArtifactId</artifactId>
<version>version,OfCourse</version>
</parent>

And that’s itt!

So now, back to the original question. Are BOM and a parent POM the same? Their packaging types are — yes. But not their purpose like you saw above. In case of a BOM, we are mainly sharing the <dependencyManagement> section amongst the POMs that import it. While in the case of a parent POM, we are sharing not just the managed dependencies, but several other configurations of the pom.xml.

Which is why you would have noticed that you import a BOM under the <dependencyManagement> tag, while you import (more specifically ‘define’) the parent POM at the very root of your project.

Moreover, a BOM contains something that can be used amongst several projects. While the parent POM’s usefulness and role is limited to the project it is created in.

And finally, you can have only one parent, but import multiple BOMs into your project.

In many cases, a BOM would already be maintaining the versions of certain dependencies, and thus again managing that dependency (and by managing, I mean the version) in your own project, might lead to a version skew and thus a dependency conflict between incompatible versions of various dependencies. The BOM could be directly managing the version of the dependency, or managing it indirectly through transitive dependencies.

To elaborate — say our BOM is managing the version of depA (as versionA1) that is in turn managing the version of its own dependency depB (as versionB1) — which means the BOM is now indirectly managing the version of depB (as versionB1). Now, if we manage the version of depB (as versionB2) directly in our POM, but versionA1 and versionB2 are incompatible with each other, we might face several issues while building our project. Which is why we could have avoided this issue altogether by omitting adding a direct dependency on depB in our POM, since depA is in any case bringing the depB along with it.

Thus, if versions of libraries are being maintained by a BOM, it would be a good practice to not have them managed in your POM again, unless of course the BOM stops managing that library in the future, which is why the Maven documentation suggests against it. For instance, if in the above example depA no longer depends on depB, but your project does, your build would fail, since you did not add a direct dependency on depB, but had relied on the BOM for that.

Okay! Enough of dependencies and POMs and BOMsss.

Dependency Tree

I will be giving a very simple overview of this.

A term that sounds scary, but is the most useful.

If you have no idea where a certain dependency is coming from, this command will be your savior.

Run mvn dependency:tree, and you can see the entire tree of dependencies of your project — not just the direct dependencies, but even the transitive dependencies of those dependencies.

This can be used in cases where a certain dependency has vulnerabilities and you are unable to track its origin.

On a side note, if different versions of the same dependency is brought in through different dependencies, only that version of a dependency will be used, which is closest to the project, in the dependency tree.

For instance, in the example cited in the previous section, versionB1 of depB was brought in by depA, and versionB2 of the same depB was directly added as a dependency in our pom.xml. In that case, the version specified directly in our POM, i.e. versionB2 is used, since that version of depB is closest in the dependency tree.

Thus if you want a specific version of a library, include it directly under <dependencies> in the POM, since it is the nearest definition.

Exclusion

Now that you have identified which dependency is bringing in the vulnerable library as a transitive dependency in the above section, it’s time to exclude that library. Use the <exclusions> tag for this purpose while including the dependency in the <dependencies> section (or alternatively in the <dependencyManagement> section — now that you know how it works). For instance:

<dependencies>
<dependency>
<groupId>groupIdOfDepBringingInTheVulnerableDep</groupId>
<artifactId>artifactIdOfThatDep</groupId>
<exclusions>
<exclusion>
<groupId>groupIdOfTheVulnerableLibrary</groupId>
<artifactId>artifactIdOfVulnerableLibrary</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

Will functionality break as we are excluding a dependency of the library? It may or may not. Yes, it will break in case the dependency being excluded is used by our project or the dependencies of our project in some way during compile/run-time/test. It won’t break, if it’s not being used. And what if it is being used? Exclude the library in the dependency that is bringing it in as a transitive dependency as shown above. And then add a direct dependency on the excluded library with a version that has no vulnerabilities. Simple.

A small note on Intellij

An IDE tool that has made life too simple!

If you want to see which libraries and their versions your project is using, just click on the ‘External Libraries’ folder which is present at the same level as your project folder. You know where these are residing in actuality though — yes, the .m2 directory.

Once new dependencies have been added to your POM, and your project built, click on the refresh icon, and you’ll see all the new libraries updated there.

There’s a lot more you can do with IntelliJ. Skipping it for now.

Conclusion

And thus, we conclude this blog.

Like I told you earlier, POM and thus Maven is a jungle, if you don’t understand how it works. But fascinating, if you do.

There’s a lot more to POM, and I had to skip several amazing details in order to prevent this blog from exploding in size.

But I do hope that this blog has thrown light on several concepts with respect to dependency management, and you find working with dependencies much easier now!

--

--