Apache Maven is popular tool for project management and defining how a project should be built. It’s flexible, extendable, and, because of it’s aforementioned popularity, most Java developers have at least some experience with it, creating a virtuous cycle of its usage.
When an enterprise adopts Maven, a frequent goal is to use it’s features and mechanisms as a way to encourage, or even enforce, enterprise standards and to make project initialization and maintenance easier. Over my career I have worked at a number of organizations, both as a developer consuming POMs defined by architects and technical leads, and as an architect defining POMs that would be consumed across an enterprise.
From this experience I have learned some of the pain points and common mistakes enterprises make when trying to use Maven. Often these pain points and mistakes are borne from a lack of knowledge of Maven’s feature set and/or incorrect usage of features. In this article we will step through how to make Maven more flexible and easier to manage in an enterprise setting.
Being a Better Parent (POM)
One of the most common ways an enterprise utilizes Maven is through the creation of an enterprise wide parent, or super, POM. Parent POMs can be a great tool for encouraging developers to follow enterprise standards and reducing bloat in child POMs be defining commonly used dependencies, plugins, reporting etc.. Parent POMs can also become a huge burden slowing down build times, bloating the size of artifacts, and causing other problems in projects that consume the parent.
Maven provides features though when defining a parent POMs that allow it to be more flexible; offering help when and where it’s needed, but also quietly stepping back when a project might have unique needs and requirements. Let’s take a look at some of these key features.
When defining a parent POM a common mistake is to add a lot dependencies that are believed to be universally required, but simply are not. Downstream this can cause problems because while it was thought every project would need dependencies X, Y, and Z, in reality some projects don’t need dependency Y, and for other projects; dependency Z causes an error when present on classpath.
Maven does provide a feature that gives a nice compromise between offering assistance with which dependencies should be used, particularly which version of a dependency should be used, and also keeping child parent POMs “clean”, and that is with Dependency Management. Dependency management is easy to use, just define dependencies as normal, but enclose them
dependencyManagement tags. Here is an example below of using dependency management:
In the child pom we need only provide the groupId and artifactId of a dependency and all the additional information: version, type, scope, is pulled down transitively.
Dependency management strikes a nice balance for both downstream developers and the architects and technical leads who are responsible for parent POMs.
Downstream developers benefit by not having to worry as much about which version of a dependency to use, what it’s scope should be, or their projects becoming bloated because a parent POM is adding extra dependencies they do not need. Downstream developers also benefit as the POMs for their projects are “cleaner” as only the artifactId and groupId for a dependency needs to be defined.
Tech leads and architects benefit by still having a centralized location from which to manage dependencies for downstream projects, but not have to deal with the issues that come from imposing the usage of dependencies, had the dependencies been defined normally within a parent POM.
The same problems with dependencies, can also happen with plugins, and for that Maven offers a similar solution with Plugin Management. When you have a frequently, but not universally, used plugin, defining the plugin with plugin management is a good option.
A common example of this would be the maven-failsafe-plugin. Every project should be using the maven-surefire-plugin to run unit tests, however not every project will need the failsafe plugin which is commonly used for executing integration tests (an example here would be a shared library which might not need to communicate with any remote systems).
By defining the maven failsafe plugin using plugin management, when a project needs to execute integration tests, they need only add groupId and artifactId for the failsafe plugin in their POM, and then all the configuration defined within the pluginManagement of the parent POM. Here is an example of using pluginManagement in a parent POM:
And then a child POM pulling down the configuration of the failsafe plugin:
Maven POMs also have section for properties. Like with any feature in Maven, and most programming languages, the most local definition of a property overrides the value that’s pulled down from a parent. Importantly though this overriding also applies to how that property is used within a parent POM. In the below example I have defined the property
my-org.version, in my parent POM and set it to
0.0.1-SNAPSHOT. Under my dependency management section I reference that property in the version field of
my-org-starter-security. This means by default the version of
my-org-starter-security will be set to
0.0.1-SNAPSHOT in any child POM. Here is the code snippet below:
However in a child POMs if the property of
my-org.version is defined and set to
0.0.2-SNAPSHOT, then that project will use version
my-org-starter-security even though the version field is not of that dependency is not defined.
This behavior would apply to anywhere where properties are used, not just to dependencies. Using properties can allow an easy way for downstream projects to change the version of dependencies they are using, as seen in the above example, or change the behavior of a plugin or reporting tool. As you are defining dependencies, plugins, etc. in parent POMs, strongly consider using properties instead of hardcoded values for fields downstream projects might want to change.
Looser Coupling with BOMs
For many enterprises parent POMs alone will be enough for handling dependency and project management concerns. However for larger enterprises, or an enterprise that works in a number of different domains, they might struggle using parent POMs alone. I experienced this at an organization where we created a whole tree of parent POMs that looked something like this chart:
There were two major problems here:
- The tree was so top heavy that a change in the top POM kicked off a lot of downstream builds
- The development cycles between the organization wide parent POM and downstream POMs were not in-sync. So this created a lot of confusion in versioning. If the “Super Parent” had a major version change, should that require a major version change in downstream POMs? If a downstream POM had a lot of changes should it also increase it’s major version or should that only be tied to upstream changes?
This confusions led to frustration for both the architecture team that I was a part of and the downstream developers as they were constantly finding issues and having to update parent POM versions.
In situations like this, it might be advantageous to use BOM’s, or Bill of Materials, so there is a looser relationship to an organization’s parent POM. A real world example of this is in the Spring framework, the Spring Cloud team publishes a BOM for managing the libraries that are associated with that project as they have a different release cadence from the Spring Boot and Spring Framework projects. You can view the BOM the Spring Cloud team produces here.
Going back the earlier example, instead of a complex tree of POMs and child POMs, a single “super parent” POM could be used and a pair of BOMs that cover the Web and Batch domains could be created. This approach likely would had been less work for the architect team that I was on, and a more pleasant experience for downstream developers as there would be fewer changes to consume.
Shared Library POMs
In a recent article I stepped through how to create a custom Spring Boot starter. When creating a library that will be shared within your enterprise it’s important to take care when defining the library’s POM. A developer’s perception of a shared library can sour quickly if using it brings in extra dependencies, causes classpath conflicts, or leads to other problems. Let’s look at some strategies for when creating a shared library’s POM.
Maven has several scopes that can be used to define when and under which circumstances a dependency should be included. The scopes are; compile, provided, runtime, and test.
- Compile: is the default scope and means that a dependency must be available when a project is being built and packages in its artifact.
- Provided: the dependency must be present when the project is being built, but will be provided by the system that will be executing the artifact.
- Runtime: the dependency isn’t required to build the artifact but must be present when running the artifact.
- Test: the dependency is only used as part of the test cycle, and not required at runtime.
Using these scopes correctly can help avoid unnecessary dependencies being brought and give developers consuming a library more control on what dependencies are being brought in.
Some dependencies might be required only in certain circumstances. In my Spring Boot starter example,
my-org-starter-security, the starter could be used in a web context and a console context. To build the project the
spring-security-web dependencies needed to present, but only needed if the project consuming
my-org-starter-security is a web project. By setting
optional console developers don’t have to worry about a bunch of extra web libraries being brought in. Below is an example of setting some dependencies as optional in a POM file:
Examples at Scale
Small scale demonstrations on how to use the features highlighted in this article is one thing, but often new problems occur at scale or once business requirements are introduced. To see examples of these concepts applied at scale I would recommend checking out how the Spring team has structured their POMs which can be found on their project GitHub repos:
Proper management of parent POMs and shared library POMs can make a world of difference in how smoothly an enterprise operates. Define them well and you have happy developers and architects who can explore more important questions. Define them poorly and you will find yourself stranded while you try desperately to fix problems.
This article focused on a few key features to make managing maven easier in most enterprise settings, it only scratches the surface of all that Maven can do however. If you want to learn more be sure to check out the official documentation, as well as this great article which steps through a full featured maven pom.
You can view the code for this article on my GitHub Repo.