Maven by Example

8.3. Optimizing Dependencies

If you look through the various POMs duplication into a parent POM.

Just as in your project’s source code, any time you have duplication in your POMs, you open the door a bit for trouble down the road. Duplicated dependency declarations make it difficult to ensure consistent versions across a large project. When you only have two or three modules, this might not be a primary issue, but when your organization is using a large, multimodule Maven build to manage hundreds of components across multiple departments, one single mismatch between dependencies can cause chaos and confusion. A simple version mismatch in a project’s dependency on a bytecode manipulation package called ASM three levels deep in the project hierarchy could throw a wrench into a web application maintained by a completely different group of developers who depend on that particular module. Unit tests could pass because they are being run with one version of a dependency, but they could fail disastrously in production where the bundle (WAR, in this case) was packaged up with a different version. If you have tens of projects using something like Hibernate Annotations, each repeating and duplicating the dependencies and exclusions, the mean time between someone screwing up a build is going to be very short. As your Maven projects become more complex, your dependency lists are going to grow, and you are going to want to consolidate versions and dependency declarations in parent POMs.

The duplication of the sibling module versions can introduce a particularly nasty problem that is not directly caused by Maven and is learned only after you’ve been bitten by this bug a few times. If you use the Maven Release plugin to perform your releases, all these sibling dependency versions will be updated automatically for you, so maintaining them is not the concern. If simple-web version 1.3-SNAPSHOT depends on simple-persist version 1.3-SNAPSHOT, and if you are performing a release of the 1.3 version of both projects, the Maven Release plugin is smart enough to change the versions throughout your multimodule project’s POMs automatically. Running the release with the Release plugin will automatically increment all of the versions in your build to 1.4-SNAPSHOT, and the release plugin will commit the code change to the repository. Releasing a huge multimodule project couldn’t be easier, until…

Problems occur when developers merge changes to the POM and interfere with a release that is in progress. Often a developer merges and occasionally mishandles the conflict on the sibling dependency, inadvertently reverting that version to a previous release. Since the consecutive versions of the dependency are often compatible, it does not show up when the developer builds, and won’t show up in any continuous integration build system as a failed build. Imagine a very complex build where the trunk is full of components at 1.4-SNAPSHOT, and now imagine that Developer A has updated Component A deep within the project’s hierarchy to depend on version 1.3-SNAPSHOT of Component B. Even though most developers have 1.4-SNAPSHOT, the build succeeds if version 1.3-SNAPSHOT and 1.4-SNAPSHOT of Component B are compatible. Maven continues to build the project using the 1.3-SNAPSHOT version of Component B from the developer’s local repositories. Everything seems to be going quite smoothly—the project builds, the continuous integration build works fine, and so on. Someone might have a mystifying bug related to Component B, but she chalks it up to malevolent gremlins and moves on. Meanwhile, a pump in the reactor room is steadily building up pressure, until something blows….

Someone, let’s call them Mr. Inadvertent, had a merge conflict in component A, and mistakenly pegged component A’s dependency on component B to 1.3-SNAPSHOT while the rest of the project marches on. A bunch of developers have been trying to fix a bug in component B all this time and they’ve been mystified as to why they can’t seem to fix the bug in production. Eventually someone looks at component A and realizes that the dependency is pointing to the wrong version. Hopefully, the bug wasn’t large enough to cost money or lives, but Mr. Inadvertent feels stupid and people tend to trust him a little less than they did before the whole sibling dependency screw-up. (Hopefully, Mr. Inadvertent realizes that this was user error and not Maven’s fault, but more than likely he starts an awful blog and complains about Maven endlessly to make himself feel better.)

Fortunately, dependency duplication and sibling dependency mismatch are easily preventable if you make some small changes. The first thing we’re going to do is find all the dependencies used in more than one project and move them up to the parent POM’s dependencyManagement section. We’ll leave out the sibling dependencies for now. The simple-parent pom now contains the following:

<project>
    ...
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring</artifactId>
                <version>2.0.7</version>
            </dependency>
            <dependency>
                <groupId>org.apache.velocity</groupId>
                <artifactId>velocity</artifactId>
                <version>1.5</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-annotations</artifactId>
                <version>3.3.0.ga</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-commons-annotations</artifactId>
                <version>3.3.0.ga</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate</artifactId>
                <version>3.2.5.ga</version>
                <exclusions>
                    <exclusion>
                        <groupId>javax.transaction</groupId>
                        <artifactId>jta</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
    </dependencyManagement>
    ...
</project>

Once these are moved up, we need to remove the versions for these dependencies from each of the POMs; otherwise, they will override the dependencyManagement defined in the parent project. Let’s look at only simple-model for brevity’s sake:

<project>
    ...
    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate</artifactId>
        </dependency>
    </dependencies>
    ...
</project>

The next thing we should do is fix the replication of the hibernate-annotations and hibernate-commons-annotations version since these should match. We’ll do this by creating a property called hibernate.annotations.version. The resulting simple-parent section looks like this:

<project>
    ...
    <properties>
        <hibernate.annotations.version>3.3.0.ga</hibernate.annotations.version>
    </properties>

    <dependencyManagement>
        ...
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-annotations</artifactId>
            <version>${hibernate.annotations.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-commons-annotations</artifactId>
            <version>${hibernate.annotations.version}</version>
        </dependency>
        ...
    </dependencyManagement>
    ...
</project>

The last issue we have to resolve is with the sibling dependencies and define the versions of sibling projects in the top-level parent project. This is certainly a valid approach, but we can also solve the version problem just by using two built-in properties—${project.groupId} and ${project.version}. Since they are sibling dependencies, there is not much value to be gained by enumerating them in the parent, so we’ll rely on the built-in ${project.version} property. Because they all share the same group, we can further future-proof these declarations by referring to the current POM’s group using the built-in ${project.groupId} property. The simple-command dependency section now looks like this:

<project>
    ...
    <dependencies>
        ...
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>simple-weather</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>simple-persist</artifactId>
            <version>${project.version}</version>
        </dependency>
        ...
    </dependencies>
    ...
</project>

Here’s a summary of the two optimizations we completed that reduce duplication of dependencies:

Pull-up common dependencies to dependencyManagement
If more than one project depends on a specific dependency, you can list the dependency in dependencyManagement. The parent POM can contain a version and a set of exclusions; all the child POM needs to do to reference this dependency is use the groupId and artifactId. Child projects can omit the version and exclusions if the dependency is listed in dependencyManagement.
Use built-in project version and groupId for sibling projects
Use ${project.version} and ${project.groupId} when referring to a sibling project. Sibling projects almost always share the same groupId, and they almost always share the same release version. Using ${project.version} will help you avoid the sibling version mismatch problem discussed previously.