Dependency Management in Spring

Updated: Jul 20


Dependency Management in Spring

In this blog, we are going to learn about how spring boot manages dependencies but before that, we will understand different types of dependencies, their scope, and versioning. Learning dependency management is a very crucial step in building a production-ready system.


You can check out the Key Takeaway section to get a quick summary of the blog.


If you are aware of the basics of dependencies, you can directly skip to the "How Spring Manages Dependency section".


Introduction

All the dependencies are defined in the pom.xml of a project. I would recommend getting yourself familiar with different pom elements if you are new or want to refresh.


Before diving into management, we should know about the different types of dependencies we will encounter in pom.


Dependencies can appear in four different forms in your pom:

  • Direct Dependencies.

  • Transitive Dependencies.

  • Inherited Dependencies

  • Dependencies of Multi-Module Project


Direct Dependencies

  • Direct dependencies are the one which you specify in the pom.

  • The maven will download them during the build

Below is an example of direct dependencies.

<dependencies>

    <!-- Spring boot starter dependency -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
        <type>jar</type>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>


    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>


    <!-- provide facility to interact with underlying database -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.microsoft.sqlserver</groupId>
        <artifactId>mssql-jdbc</artifactId>
        <version>${sqlserver.version}</version>
    </dependency>


    <!-- provide framework for testing -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>
</dependencies>.

Transitive Dependencies

  • Transitive dependencies are a part of direct dependencies; we don't specify it in POM.

  • They will be downloaded along with direct dependencies by maven.

  • We can also exclude few or all transitive dependencies by setting a value for the exclusion element. Check below for an example.

The below example shows how to exclude a single transitive dependency.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
    <type>jar</type>
</dependency>

The below example will exclude all transitive dependencies. You may need to specify excluded dependency manually if it is required.

<dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-embedder</artifactId>
    <version>3.1.0</version>
    <exclusions>
        <exclusion>
            <groupId>*</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Inherited Dependencies

  • Inherited Dependencies are the one which will be inherited in Child POM from parent POM.

  • The concept of Inherited Dependency is similar to Inheritance in Object-Oriented Programming.

  • Almost all element of parent pom is inherited by child except name, artifactId, and prerequisite. Check out this blog to learn more about the pom.

  • When we have poms in the Parent-Child relationship, then you should set parent pom.xml packaging as pom.

  • Usually, a module containing parent pom doesn't contain any classes/ Interface or other objects; it only has a pom.xml file with packaging as pom.

parent pom

Multi-Module Project Dependencies

  • In the multi-module project, we list all the modules in the parent pom. Check out this blog to learn more about the multi-module project.

  • Order of specifying module does not matter; maven will take care of everything.

  • In a multi-module project, parent pom needs to be packaged as pom.

<modules>
    <module>lib/logging</module>
    <module>lib/security</module>
    <module>lib/exception</module>
    <module>backend-starter</module>
    <module>lib/api-docs</module>
    <module>lib/utility</module>
    <module>lib/notification</module>
    <module>lib/file-handler</module>
</modules>

Before going any further, we should familiarize ourselves with the concept of classpath since, based on the scope, different dependencies will be saved in the different classpaths.


Classpath

Classpath is the location where maven puts the .class and .jar files after build. There are three types of classpaths in maven dependency management:

  • compile-classpath: It contains all the classes that we have coded to compile the code.

  • Runtime-classpath: It contains all the classes that are required during runtime only.

  • Test-classpath: It is also similar to runtime classpath, but instead of regular classes, it includes classes that you have coded for performing testing.

How to Specify Dependency in your pom

<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">
...
<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <type>jar</type>
        <scope>test</scope>
        <optional>true</optional>
    </dependency>
    ...
</dependencies>
...
</project>
  • groupId, artifactId, and version are like an address for the dependency.

  • WHAT type of dependency we are adding is decided by setting a value to the type tag. It tells us what type of packaging is done on dependency.

  • Scope of the dependency means during which build phase this dependency will be used and on which classpath it will be available. We set the scope by setting the value to the scope tag. There are five types of scope for dependency:

  • compile: these dependencies are required for development, compilation, and runtime.

  • this is the default scope. All the dependency with the scope of compile will be available in all classpaths as mentioned above.

  • All the transitive dependencies (only compile and runtime scoped) of compile scoped direct dependency are downloaded by maven without changing their scope.

  • e.g., logging dependencies.

  • provided: these dependencies are generally required during development and compilation, sometimes they are also required during runtime.

  • dependency with the scope provided will be available only in compile and test classpath. JDK or container in which our program is running is expected to give the dependency during runtime when required.

  • All the transitive dependencies (only compile and runtime scoped) of provided scoped dependencies are downloaded by maven as a provided scoped dependencies.

  • e.g., Application Server Dependency, Lombok

  • runtime: these dependencies are only required during runtime, they won't get compiled during compilation.

  • dependency with the scope of the runtime is only available at runtime-classpath and test-classpaths.

  • All the transitive dependencies (only compile and runtime scoped) of runtime scoped maven downloads dependencies as runtime scoped dependencies.

  • e.g., Database Dependencies

  • test: these dependencies are only required during Testing

  • dependency with the scope of the test is only available at test-classpath.

  • All the transitive dependencies (only compile and runtime scoped) of test scoped dependency are downloaded by maven as test scoped dependencies.

  • e.g., Unit Test Dependencies

  • system: these dependencies are similar to provided and only utilized in development and compilation.

  • dependency with the scope of the system is similar to dependency with the scope provided with a difference of location, i.e., maven will look for these dependencies in local repositories.

  • You should usually avoid setting system scoped dependency.

Different Scope of a dependency

packaged jar doesn't contains any provided and test scoped dependencies.


Versioning Requirement in Dependency

Using an appropriate version of a dependency is very necessary to avoid build failure and security check failure.


There are two types of versioning in Dependencies:

  • Soft Requirement: Different versions of the same dependency can be used if needed.

  • Hard Requirement: Only a specified version is needed; else the build will fail.


Syntax of version requirement:

Versioning syntax is very similar to sets and inequality in maths. Open Bracket ( "(" , ")") means number is not included , Square brackets ( "[", "]") means number is included.

  • 2.0: It is a soft requirement that means any other version can also be used if it appears earlier in the dependency tree. The build will fail if neither 2.0 is present nor any different version in the dependency tree.

  • [2.0]: It is a hard requirement that if this version is not available, then the build will fail.

  • [2.1, 2.5]: It is a hard requirement that maven can only download dependency between versions 2.1 and 2.5. Maven will download the version that comes earlier in the dependency tree.

  • (2.1, 2.5]: maven can only download 2.1 < version <= 2.5.

  • [2.1, ]: any dependency with version greater than or equal to 2.1.

  • ( , 2.3 ] , [ 2.5, ): any dependency with 2.3 <= version or version >= 2.5 but not 2.4.

Maven picks the highest version of each project that satisfies all the hard requirements of the dependencies on that project. If no version satisfies all the hard requirements, the build fails

mvn dependency:list  will display all the dependencies as a list.
mvn dependency:tree will display all the dependencies along with transitive dependencies.

How does Maven Manages Dependencies

  • Maven will download all of the transitive dependencies automatically. There is no limit on how deep maven can go to download transitive dependency.

  • If Multiple Dependencies of different versions are encountered, then maven will download only one version by using the shortest path algorithm.

Dependency Tree