Coordinates, Dependencies, Repositories, Metadata, Transitivity, and Version Selection: Guided Hands-On Workflow and Core Operations
Resolve and inspect the same small dependency family with Maven and Gradle, observe a real transitive version conflict, isolate caches safely, and prove which bytes and metadata were selected.
Learning objectives
- Create equivalent Maven and Gradle dependency declarations for one small, verifiable dependency family.
- Capture the resolved graph and explain why Maven selects commons-lang3 3.9 while Gradle selects 3.12.0 by default.
- Use dependency-tree and dependencyInsight evidence instead of inferring the winner from build-file order.
- Repeat dependency resolution with isolated Maven/Gradle local state without deleting normal user caches.
- Record repository-origin, graph, and artifact/checksum evidence suitable for a CI handoff.
org.apache.commons:commons-text:1.10.0, whose published
POM declares org.apache.commons:commons-lang3:3.12.0,
plus a deliberate direct request for commons-lang3:3.9.
Re-check current versions and metadata before reusing these examples
in production.
1. Disposable scenario and preflight
Create two sibling directories, one Maven and one Gradle, so tool-specific state does not collide. The lab uses only Maven Central and two small Apache Commons modules. If the network is unavailable, use the embedded expected-output fixtures to practice graph interpretation; do not weaken TLS, add random mirrors, or disable verification controls to make the download work.
mkdir -p dependency-lab/maven dependency-lab/gradle
cd dependency-lab
java -version
# Run the wrapper version command inside each prepared project.
# Windows PowerShell: use New-Item -ItemType Directory and .\mvnw.cmd / .\gradlew.bat.
2. Maven track — declare the graph explicitly
The POM declares both commons-text:1.10.0 and
commons-lang3:3.9. The first declaration contributes a
transitive request for lang3 3.12.0. The second is direct. Maven
therefore sees two requests for one module.
<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>
<groupId>com.example.academy</groupId>
<artifactId>dependency-lab-maven</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.10.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
cd dependency-lab/maven
./mvnw -v
./mvnw -Dmaven.repo.local=.lab-m2-repository org.apache.maven.plugins:maven-dependency-plugin:3.10.0:tree -Dincludes=org.apache.commons:commons-text,org.apache.commons:commons-lang3
./mvnw -Dmaven.repo.local=.lab-m2-repository org.apache.maven.plugins:maven-dependency-plugin:3.10.0:tree -DoutputType=json -DoutputFile=dependency-tree.json
com.example.academy:dependency-lab-maven:jar:1.0.0
+- org.apache.commons:commons-text:jar:1.10.0:compile
| \- (org.apache.commons:commons-lang3:jar:3.12.0:compile - omitted for conflict with 3.9)
\- org.apache.commons:commons-lang3:jar:3.9:compile
Selected: commons-lang3 3.9
Reason: direct request is nearer to the project than commons-text -> lang3 3.12.0
The exact text annotation can vary by plugin output/version. The invariant is the resolved selected version and graph path, not punctuation in decorative CLI output.
3. Gradle track — declare the same requests
The Gradle project asks for the same module versions, but Gradle’s default conflict policy evaluates all requested versions and selects the higher one. The build script uses Kotlin DSL because later course chapters teach both DSLs explicitly; the resolution model is not specific to Kotlin syntax.
plugins {
java
}
group = "com.example.academy"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation("org.apache.commons:commons-text:1.10.0")
implementation("org.apache.commons:commons-lang3:3.9")
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
tasks.withType<JavaCompile>().configureEach {
options.release = 17
}
cd dependency-lab/gradle
./gradlew -version
./gradlew dependencies --configuration runtimeClasspath
./gradlew dependencyInsight --dependency org.apache.commons:commons-lang3 --configuration runtimeClasspath
runtimeClasspath
+--- org.apache.commons:commons-text:1.10.0
| \--- org.apache.commons:commons-lang3:3.12.0
\--- org.apache.commons:commons-lang3:3.9 -> 3.12.0
Selection reason includes conflict resolution.
Selected: commons-lang3 3.12.0
dependencyInsight is more important than the arrow
notation: it explains the selection reason and which paths requested
the module.
4. Prove repository origin and artifact identity without scraping UI
For a simple Central-only lab, the declared repository boundary is straightforward. In production, effective Maven settings/mirrors or Gradle repository declarations/content filters can redirect where metadata/artifacts are fetched. Record effective repository policy along with the graph.
./mvnw help:effective-settings -Doutput=effective-settings.xml
./mvnw help:effective-pom -Doutput=effective-pom.xml
# Inspect <mirrors>, <profiles>/<repositories>, and effective project repositories.
./gradlew buildEnvironment
# For this lab the build declares only mavenCentral().
# In a production build, inspect settings-level dependencyResolutionManagement too.
After resolution, compute a checksum of the selected JAR in the isolated lab cache, not by assuming a path in your normal user home. The checksum identifies the bytes you inspected; it does not by itself prove provenance or safety.
# Maven isolated repository example:
find .lab-m2-repository -path '*commons-lang3*/*.jar' -print
sha256sum $(find .lab-m2-repository -path '*commons-lang3*/*.jar' -print | head -n 1)
# Gradle cache layout is implementation detail; prefer build reports for automation.
# If inspecting a disposable GRADLE_USER_HOME, enumerate before hashing and record exact path.
5. Warm state versus isolated state
First run normally within the lab’s isolated state. Run again and note reduced network work. Then point Maven/Gradle at a second empty disposable state directory. The goal is not “delete cache until it works.” The goal is to ask whether a clean resolver can still obtain the same immutable metadata/artifacts.
./mvnw -Dmaven.repo.local=.lab-m2-fresh org.apache.maven.plugins:maven-dependency-plugin:3.10.0:tree
GRADLE_USER_HOME="$PWD/.lab-gradle-home-fresh" ./gradlew dependencies --configuration runtimeClasspath
# PowerShell:
# $env:GRADLE_USER_HOME = "$PWD/.lab-gradle-home-fresh"
# .\gradlew.bat dependencies --configuration runtimeClasspath
If the fresh run fails while the warm run succeeds, preserve that evidence. It can indicate network/repository availability, removed artifacts, credentials, mirror policy, or changing metadata—not automatically a corrupt cache.
6. Make the conflict intentional instead of accidental
Now choose a policy. Suppose the application team decides
commons-lang3:3.12.0 is the approved version. In Maven,
a direct declaration or dependency management can make that
explicit. In Gradle, a direct declaration, constraint/platform, or
strict version policy can encode the intent. This chapter uses the
smallest visible controls; later chapters go deeper into Maven
dependencyManagement/BOMs and Gradle platforms/constraints.
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
dependencies {
implementation("org.apache.commons:commons-text:1.10.0")
implementation("org.apache.commons:commons-lang3:3.12.0")
}
Rerun the graph commands and verify both tools now select 3.12.0 because the project model expresses the same approved intent. Do not rely on transitive coincidence.
7. Small challenge — choose the control from the evidence
You inherit a Gradle build where
dependencyInsight shows 3.12.0 selected because another
library requests it, but your source imports lang3 classes directly.
What should you change first: repository order, cache TTL, or the
project dependency declaration?
Answer: declare the dependency your source uses directly. Repository/caching changes do not express ownership of the API contract. Then apply version governance with the appropriate later-course mechanism.
8. Verification checklist and cleanup
[ ] Maven wrapper/tool identity recorded
[ ] Gradle wrapper/tool identity recorded
[ ] Maven graph captured; selected lang3 version explained
[ ] Gradle graph + dependencyInsight captured; selection reason explained
[ ] repository policy/origin assumptions recorded
[ ] second isolated Maven repository resolves expected immutable modules
[ ] second isolated Gradle User Home resolves expected immutable modules
[ ] intentional alignment rerun captured
[ ] no normal ~/.m2 or ~/.gradle state deleted
pwd
# Inspect first; these names are disposable lab state only:
ls -ld .lab-m2-repository .lab-m2-fresh .lab-gradle-home-fresh 2>/dev/null || true
# Delete only after confirming you are inside dependency-lab:
# rm -rf .lab-m2-repository .lab-m2-fresh .lab-gradle-home-fresh
Knowledge check
Why does Maven select lang3 3.9 in the initial lab?
Because 3.9 is a direct dependency at depth one, while 3.12.0 is transitive through commons-text; Maven nearest-definition mediation chooses the nearer request.
Why does Gradle select 3.12.0 in the initial lab?
Both versions are requested for the same module and Gradle’s default version-conflict behavior selects the higher requested version.
What does an isolated fresh dependency home test?
Whether the build can reproduce dependency resolution without relying on previously cached metadata/artifacts from the learner’s normal environment.
Why should source code that directly imports a transitive library usually declare it directly?
It documents and stabilizes the project’s actual API dependency so an upstream library can change its own transitive graph without unexpectedly removing your compile contract.
Does a SHA-256 checksum prove the library is safe?
No. It identifies bytes. Trust/provenance, signature/verification policy, license, vulnerability status, and repository governance are separate questions.
Summary
You have turned an invisible download process into evidence: declared requests, transitive edge, selected version, selection reason, repository policy, cache state, and artifact identity. The Maven/Gradle difference is now observable rather than memorized.
Official references and version notes
These lessons were finalized against current primary documentation on 2026-08-23. Dependency metadata, plugin versions, repository policies, Gradle resolution behavior, and Maven/Gradle releases are version-sensitive. Verify the exact tool versions and repository policy used by your project and CI before applying production controls.
- Apache Maven — Releases History
- Maven — Introduction to the Dependency Mechanism
- Maven — Setting up Multiple Repositories
- Maven — Using Mirrors for Repositories
- Apache Maven Dependency Plugin
- Gradle 9.7.1 — Dependency Resolution
- Gradle 9.7.1 — Graph Resolution
- Gradle 9.7.1 — Viewing and Debugging Dependencies
- Gradle 9.7.1 — Declaring Versions and Ranges
- Gradle 9.7.1 — Dependency Caching
- Maven Central — commons-text 1.10.0 metadata
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.