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.
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
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.
- billing-app 1.0.0 declares two dependencies: legacy-audit 1.4.0 first and invoice-json 2.0.0 second.
- 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.
- 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.
- 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:
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 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-auditis declared first. - Swapping two lines changed the answer.
reordered.xmlispom.xmlwith 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,LATESTorRELEASE, 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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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).
-
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.
-
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?
-
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?
-
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?
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 -Dverboseorgradle dependencieswhenever a dependency changes. - Pin exact versions, keep families together with a BOM, and let the build check checksums and look up advisories.
References
- Introduction to the Dependency Mechanism (The Apache Software Foundation)
- Apache Maven Dependency Plugin: dependency:tree (The Apache Software Foundation)
- POM Reference: repositories, checksumPolicy and updatePolicy (The Apache Software Foundation)
- Maven CLI Options Reference (3.10.0): --strict-checksums (The Apache Software Foundation)
- Maven Resolver: Expected Checksums and Trusted Checksums (The Apache Software Foundation)
- Maven Resolver: Remote Repository Filtering (The Apache Software Foundation)
- Apache Maven Enforcer: Built-In Rules (The Apache Software Foundation)
- Configuring for Reproducible Builds (The Apache Software Foundation)
- Graph Resolution: version conflicts (Gradle)
- Viewing and Debugging Dependencies (Gradle)
- Platforms (Gradle)
- ResolutionStrategy: failOnVersionConflict() (Gradle)
- Locking Dependency Versions (Gradle)
- Verifying Dependencies (Gradle)
- Filtering Repository Content (Gradle)
- Versions Maven Plugin: versions:display-dependency-updates (MojoHaus)
- Maven Central: Requirements (Sonatype)
- OSV: a distributed vulnerability database for open source (OSV)
- OSV API: POST /v1/querybatch (OSV)
- Apache Commons Text: Security Vulnerabilities (CVE-2022-42889) (The Apache Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress