Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 14 – Testing, build tools and code quality

Dependency management and supply-chain safety

Resolve Java dependency conflicts in Maven and Gradle, pin versions with BOMs and lock files, check what you download and look up known vulnerabilities.

  • Advanced
  • 28 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 9 of 28 minutes free
Free: One tree, two answers · Maven and Gradle, side by side · The safeguards, by name · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 19 minutes.

What you will learn

  • Read Maven and Gradle dependency trees and explain which version of a library each tool keeps
  • Resolve version conflicts with a BOM, dependency management and rules that fail the build
  • Configure exact versions and Gradle dependency locking so that builds are reproducible
  • Check downloaded artifacts and look up the resolved versions for known vulnerabilities

Before you start

What you’ll learn · Before you start

What you will learn

  • Read Maven and Gradle dependency trees and explain which version of a library each tool keeps
  • Resolve version conflicts with a BOM, dependency management and rules that fail the build
  • Configure exact versions and Gradle dependency locking so that builds are reproducible
  • Check downloaded artifacts and look up the resolved versions for known vulnerabilities

Before you start

On this page

Dependency management is deciding which libraries a project uses and which exact version of each one ends up on the class path, including the libraries that those libraries bring with them. Supply-chain safety is making sure that every one of those files is the file its authors published, and that none of them has a known vulnerability, on every build. Both matter because a project that declares two libraries can receive a dozen. When two of them need different versions of the same library, the build tool keeps one, and Maven and Gradle choose by different rules. A wrong choice compiles, passes the tests and fails in production, or brings back a version with published security advisories. Look at the tree whenever you add or upgrade a dependency, and look up its versions whenever a new advisory appears.

This lesson builds one small system with Apache Maven 3.10.0 and Gradle 9.7.1 on Eclipse Temurin 25.0.4.1. Two in-house libraries, published by other teams into a company repository, were built against different versions of the JSON library jackson-databind. A billing application uses both. Its Gradle builds are written in Gradle’s Groovy DSL, build.gradle, which drives the same API as the Kotlin DSL of the previous lesson.

One tree, two answers

billing-app needs two libraries that need jackson-databind 2.13.2 and 2.22.3; Maven keeps 2.13.2 and Gradle keeps 2.22.3.billing-app 1.0.0legacy-audit 1.4.0declared firstinvoice-json 2.0.0declared secondjackson-databind 2.13.2jackson-databind 2.22.3Maven keeps 2.13.2nearest, then first declaredGradle keeps 2.22.3the highest requestedneedsneeds

Two requests for one library, and the version each build tool keeps

Text description of the diagram

The diagram shows the dependency tree of the billing application in this lesson.

  1. billing-app 1.0.0 declares two dependencies: legacy-audit 1.4.0 first and invoice-json 2.0.0 second.
  2. legacy-audit 1.4.0 needs jackson-databind 2.13.2, and invoice-json 2.0.0 needs jackson-databind 2.22.3. Both requests sit two levels below the application.
  3. A dashed arrow leads from jackson-databind 2.13.2 to a box that reads "Maven keeps 2.13.2: nearest, then first declared". Both versions are equally near, so the one reached through the first declared dependency wins.
  4. A dashed arrow leads from jackson-databind 2.22.3 to a box that reads "Gradle keeps 2.22.3: the highest requested".

The class path of the application can hold only one jackson-databind, so the same declarations give a different application depending on the build tool.

legacy-audit 1.4.0 needs jackson-databind 2.13.2 and invoice-json 2.0.0 needs 2.22.3. Both requests sit two levels below the application, and its class path can hold only one jackson-databind:

The billing application's two builds, and the script that publishes the libraries

app/pom.xml · app/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- The billing application: it uses both in-house libraries and never names Jackson itself. -->
<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>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>company-parent</artifactId>
    <version>1.0.0</version>
    <relativePath>../company-parent/pom.xml</relativePath>
  </parent>
  <artifactId>billing-app</artifactId>
  <version>1.0.0</version>

  <repositories>
    <!-- The company's repository, where the in-house libraries are published. -->
    <repository>
      <id>company</id>
      <url>file:${project.basedir}/../repo</url>
    </repository>
  </repositories>

  <dependencies>
    <dependency>
      <groupId>com.example</groupId>
      <artifactId>legacy-audit</artifactId>
      <version>1.4.0</version>
    </dependency>
    <dependency>
      <groupId>com.example</groupId>
      <artifactId>invoice-json</artifactId>
      <version>2.0.0</version>
    </dependency>
  </dependencies>
</project>

app/build.gradle · app/build.gradle

// The same billing application, built by Gradle from the same sources (Groovy DSL).
plugins {
    id 'application'
}

group = 'com.example'
version = '1.0.0'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

repositories {
    // The company's repository, where the in-house libraries are published, then Maven Central.
    maven {
        name = 'company'
        url = uri('../repo')
    }
    mavenCentral()
}

dependencies {
    implementation 'com.example:legacy-audit:1.4.0'
    implementation 'com.example:invoice-json:2.0.0'
}

application {
    mainClass = 'com.example.billing.Main'
}

publish-libs.sh

#!/bin/sh
# Publishes projects into the file repository repo/ beside this script, the way a release build publishes them into
# a company's repository manager. With no arguments: the parent POM and the two in-house libraries.
set -e
cd "$(dirname "$0")"
repo="file:$(pwd)/repo"
[ $# -gt 0 ] || set -- company-parent libs/legacy-audit libs/invoice-json
for project in "$@"; do
  mvn --quiet --batch-mode --file "$project/pom.xml" deploy -DaltDeploymentRepository="company::$repo" -Dmaven.install.skip=true
  echo "published $project"
done
The same tree resolved by Maven, by Maven with the two dependencies swapped, and by Gradle Python · tree_session.py
from session import run

# network: true (Maven Central). Two teams publish their libraries into the company repository; then the billing
# application, which depends on both, is resolved by Maven, by Maven with its two dependencies in the other order,
# and by Gradle.
run(
    """
sh publish-libs.sh
""",
    show=("java", "maven", "gradle"),
)
run(
    """
mvn -q dependency:tree -Dverbose -DoutputFile=tree.txt
cat tree.txt
mvn -q -f reordered.xml dependency:tree -Dverbose -DoutputFile=tree.txt
cat tree.txt
gradle -q dependencies --configuration runtimeClasspath
""",
    folder="app",
)

Output

# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0 · Gradle 9.7.1
$ sh publish-libs.sh
published company-parent
published libs/legacy-audit
published libs/invoice-json
$ cd app
$ mvn -q dependency:tree -Dverbose -DoutputFile=tree.txt
$ cat tree.txt
com.example:billing-app:jar:1.0.0
+- com.example:legacy-audit:jar:1.4.0:compile
|  \- com.fasterxml.jackson.core:jackson-databind:jar:2.13.2:compile
|     +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.2:compile
|     \- com.fasterxml.jackson.core:jackson-core:jar:2.13.2:compile
\- com.example:invoice-json:jar:2.0.0:compile
   \- (com.fasterxml.jackson.core:jackson-databind:jar:2.22.3:compile - omitted for conflict with 2.13.2)
$ mvn -q -f reordered.xml dependency:tree -Dverbose -DoutputFile=tree.txt
$ cat tree.txt
com.example:billing-app:jar:1.0.0
+- com.example:invoice-json:jar:2.0.0:compile
|  \- com.fasterxml.jackson.core:jackson-databind:jar:2.22.3:compile
|     +- com.fasterxml.jackson.core:jackson-annotations:jar:2.22:compile
|     \- com.fasterxml.jackson.core:jackson-core:jar:2.22.3:compile
\- com.example:legacy-audit:jar:1.4.0:compile
   \- (com.fasterxml.jackson.core:jackson-databind:jar:2.13.2:compile - omitted for conflict with 2.22.3)
$ gradle -q dependencies --configuration runtimeClasspath

------------------------------------------------------------
Root project 'billing-app'
------------------------------------------------------------

runtimeClasspath - Runtime classpath of source set 'main'.
+--- com.example:legacy-audit:1.4.0
|    \--- com.fasterxml.jackson.core:jackson-databind:2.13.2 -> 2.22.3
|         +--- com.fasterxml.jackson.core:jackson-annotations:2.22
|         +--- com.fasterxml.jackson.core:jackson-core:2.22.3
|         |    \--- com.fasterxml.jackson:jackson-bom:2.22.3
|         |         +--- com.fasterxml.jackson.core:jackson-annotations:2.22 (c)
|         |         +--- com.fasterxml.jackson.core:jackson-core:2.22.3 (c)
|         |         \--- com.fasterxml.jackson.core:jackson-databind:2.22.3 (c)
|         \--- com.fasterxml.jackson:jackson-bom:2.22.3 (*)
\--- com.example:invoice-json:2.0.0
     \--- com.fasterxml.jackson.core:jackson-databind:2.22.3 (*)

(c) - A dependency constraint, not a dependency. The dependency affected by the constraint occurs elsewhere in the tree.
(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.

A web-based, searchable dependency report is available by adding the --scan option.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tree_session.py

  • Maven kept 2.13.2. Its tree marks the other request “omitted for conflict with 2.13.2”. Maven’s dependency guide gives the rule: the nearest definition wins, and when two versions are equally near, the first declaration wins. legacy-audit is declared first.
  • Swapping two lines changed the answer. reordered.xml is pom.xml with its two dependencies in the other order, and Maven kept 2.22.3. The order of declarations in a POM is part of what the build produces.
  • Gradle kept 2.22.3. Its report shows 2.13.2 -> 2.22.3: Gradle considers every requested version in the graph and, by default, selects the highest. It also applied the constraints that Jackson’s own metadata declares, marked (c), so jackson-core and jackson-annotations moved to the same release.

Neither tool printed a warning. The same declarations gave two different applications, and nothing failed until one of them ran.

Text Diff Paste two dependency trees, before and after an upgrade, to see every version that moved.

Maven and Gradle, side by side

Task Maven Gradle
Settle a conflict The nearest definition; the first declared when equally near The highest version requested anywhere
Show why a version won dependency:tree -Dverbose dependencyInsight --dependency <name>
Fix one version for the whole tree dependencyManagement, or a BOM imported into it A constraint, or a BOM as a platform(...)
Fail instead of choosing The enforcer rule dependencyConvergence failOnVersionConflict()
Record the exact versions Exact versions and enforcer rules (no lock file) Dependency locking, in gradle.lockfile
Refuse a replaced file Trusted checksums, recorded in the project gradle/verification-metadata.xml

The rows follow each tool’s documentation, cited below. Dependency management in both tools comes down to the same three habits: read the tree, pin the versions you depend on, and make the build fail when something drifts.

The safeguards, by name

  • Exact versions. A version such as 2.22.3, never a range, LATEST or RELEASE, so one commit always resolves the same libraries.
  • A BOM. A bill of materials is a POM that lists matching versions for a family of modules, such as every Jackson module, imported once.
  • Exclusions. An <exclusion> removes one transitive dependency from one path of the tree, so the version that another path brings is the only one left.
  • Dependency locking. Gradle writes every resolved version into gradle.lockfile, and later builds keep those versions until someone updates the lock on purpose.
  • Checksums and signatures. Maven Central requires a checksum file and a PGP signature beside every file it accepts, and the build tools compare the checksums when they download.
  • Pinned hashes. Hashes of every artifact, recorded in your own repository, so that a file replaced on a server fails the build.
  • Vulnerability lookups. A scanner looks up each resolved version in an advisory database, such as OSV, and lists the advisories that match.
  • Repository filtering. Rules that say which repository may serve which group, so an in-house name is never fetched from a public repository.

Supply-chain safety is these habits applied on every build, by the build itself, rather than remembered by people.

Hash Generator Compute the SHA-256 or SHA-512 of a downloaded JAR to compare it with the checksum its publisher lists.

Check yourself

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 legacy-audit, declared first, needs jackson-databind 2.13.2, and invoice-json, declared second, needs 2.22.3. Both requests are two levels below the application. Which version does Maven put on the class path?

    Choose one answer.

    Show the answer to question 1

    Answer: 2.13.2, because the versions are equally near and legacy-audit is declared first

    Maven keeps the nearest definition, and when two are equally near, the first declaration wins. The recorded tree marks the other request "omitted for conflict with 2.13.2". A class path holds one version of a library, and Maven does not stop on a conflict unless a rule such as the enforcer's dependencyConvergence tells it to.

  2. Question 2 of 4 Gradle resolves the same two dependencies with its default rules. Which jackson-databind does it keep?

    Choose one answer.

    Show the answer to question 2

    Answer: 2.22.3, the highest version requested anywhere in the graph

    Gradle considers every requested version and, by default, selects the highest. The recorded report shows it as "2.13.2 -> 2.22.3" under legacy-audit.

  3. Question 3 of 4 reordered.xml is pom.xml with its two dependencies in the other order and nothing else changed. What did that change?

    Choose one answer.

    Show the answer to question 3

    Answer: Maven kept jackson-databind 2.22.3 instead of 2.13.2

    With both requests equally near, the first declared dependency decides, so swapping the two lines swapped the version. The order of a POM's dependencies is part of what the build produces.

  4. Question 4 of 4 Which of these make one commit resolve the same versions on every build? Choose every one that does.

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • A committed Gradle lock file
    • An exact version such as 2.22.3

    An exact version and a lock file both name one version. A range and LATEST are resolved anew on each build, so the next release of the library changes the result without any change to the project.

Version conflicts in depth: the failure, four fixes and builds that refuse to guess Pro: opens with a Pro, Premium or Ultimate pass

A version conflict usually shows itself far from its cause, in a stack trace from a library the application never calls directly. This part reproduces that failure, compares the fixes and makes both tools stop instead of choosing.

In this part:

  • The conflict at run time, reproduced
  • Four fixes and their trade-offs
  • Builds that refuse to guess

3 runnable examples (Java, Python) · about 2 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Reproducible versions: a range that moves, BOMs and lock files, measured Pro: opens with a Pro, Premium or Ultimate pass

A build is reproducible when the same commit resolves the same versions on every computer, today and next month. This part shows a version range moving without any change to the project, and the two ways to hold it still.

In this part:

  • A version range moves when a library is released, measured
  • A lock file holds a range still, measured
  • Which pin holds what
  • Updating on purpose, on a schedule

4 runnable examples (GRADLE, Python, XML) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Supply-chain drills: a replaced JAR, pinned hashes and known vulnerabilities Pro: opens with a Pro, Premium or Ultimate pass

A checksum from the repository proves that a download arrived as the repository holds it, not that the repository holds what the author published. These drills damage and then replace a library in the company repository, and show which settings notice.

In this part:

  • A damaged download: the default only warns, measured
  • A replaced library with matching checksums, reproduced
  • Known vulnerabilities, looked up for the exact versions
  • Exercise: repair the report job

8 runnable examples (Java, Python, XML) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

  1. Fresher to mid · Warm-up

    How does Maven choose a version when two dependencies need different versions of the same library?

    Maven keeps exactly one version, by a rule called dependency mediation: the nearest definition wins, meaning the version declared closest to your project in the dependency tree. When two versions are equally near, the one reached through the dependency declared first in the POM wins. The other request is dropped, and dependency:tree -Dverbose shows it as omitted for conflict with the kept version. A version your own POM declares, directly or in dependencyManagement, is always the nearest, which is how you choose one on purpose. Gradle uses another rule by default: it keeps the highest version requested anywhere in the graph, so the same declarations can give the two tools different class paths. Neither tool warns by default; the enforcer rule dependencyConvergence, or failOnVersionConflict() in Gradle, turns a conflict into a failed build.

  2. Mid to senior

    After your team adds one in-house library, a service passes all its tests but throws NoSuchMethodError from inside a JSON library in production. Nobody changed the JSON library's version in the POM. How do you find the cause, fix it, and keep it from coming back?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    A CI token that could publish to your company's Maven repository leaked and was usable for two days. How do you find out whether any artifact your services build with was replaced, and what do you change so that a replaced artifact fails the build next time?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    On a Friday evening the scanner reports 16 advisories in a payment service's dependencies, one rated critical in a library you never declared. How do you decide what to fix now and what can wait, and how do you change the version of a library you do not declare?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • One library can only have one version on a class path: Maven keeps the nearest, then the first declared, and Gradle keeps the highest.
  • The order of two dependencies in a POM changed which jackson-databind the application received.
  • Read the tree with dependency:tree -Dverbose or gradle dependencies whenever a dependency changes.
  • Pin exact versions, keep families together with a BOM, and let the build check checksums and look up advisories.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.