Chapter 03Lesson 02~115 minutes

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.

MavenGradleDependency GraphCache IsolationEvidence

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.
Version baseline — verified 2026-08-23. Examples use JDK 21 as the common lab runtime, Apache Maven 3.9.16, Apache Maven Wrapper 3.3.4 where a Maven wrapper is already present, Apache Maven Dependency Plugin 3.10.0 for dependency-tree evidence, and Gradle 9.7.1 through the Gradle Wrapper. Maven 4.0.0-rc-6 is still a release candidate and is not required here. The illustrative graph uses 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.
Preflight: use JDK 21; use the existing project wrappers when present. Do not copy wrapper JARs/scripts from arbitrary websites. The dependency examples compile no application code, so the lab is primarily a resolution/model exercise.

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?

Why does Gradle select 3.12.0 in the initial lab?

What does an isolated fresh dependency home test?

Why should source code that directly imports a transitive library usually declare it directly?

Does a SHA-256 checksum prove the library is safe?

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.

Next lesson

Choose dependency and repository policy deliberately

Lesson 3 evaluates fixed versus dynamic versions, direct declarations, Maven mediation/management, Gradle constraints/resolution, and public versus controlled repository topology.

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.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.