Chapter 03Lesson 01~85 minutes

Coordinates, Dependencies, Repositories, Metadata, Transitivity, and Version Selection: Concepts, Architecture, and Mental Model

Build a dependency-resolution mental model that separates coordinates, metadata, artifacts, repositories, transitivity, conflict rules, caches, and the final resolved classpath.

CoordinatesMetadataTransitivityVersion SelectionRepositories

Learning objectives

  • Read a dependency coordinate as an identity request rather than as a filename or a download URL.
  • Separate repository metadata from artifact bytes and explain how metadata expands direct declarations into a transitive graph.
  • Distinguish direct, transitive, dynamic/ranged, snapshot/changing, classifier, and variant concepts at a beginner-safe level.
  • Explain why Maven and Gradle can receive the same version requests yet select different resolved versions.
  • Inspect declared and resolved dependency state before changing versions, repositories, or caches.
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. The practical problem: your source code names libraries, but the build consumes a resolved graph

Chapter 02 showed where dependencies eventually land: compile, runtime, test, and packaging classpaths. Chapter 03 explains where those dependency files come from and why the classpath can contain libraries you never typed into the build file.

A dependency declaration is not “download this JAR.” It is a request to a resolution engine. The engine combines a module identity, one or more version requirements, repository metadata, transitive edges, conflict rules, requested usage/variant information, local cache state, and repository policy. Only after that model is resolved can the tool select actual artifact files.

Trust boundary: dependency and plugin resolution downloads metadata and executable code from external or internal repositories. A successful resolution proves only that the resolver found acceptable files—not that those files are safe, correctly governed, or free of vulnerabilities.

2. Coordinates identify a module; artifact files are outputs of that identity

In Maven-compatible repositories, the common identity tuple is groupId:artifactId:version. Gradle commonly writes the same idea as group:name:version. Packaging/type and classifier can select additional artifacts associated with that module/version. Gradle adds a richer variant model: the consumer can request attributes such as usage or target platform, and metadata can describe multiple variants/capabilities.

Concept Maven-oriented wording Gradle-oriented wording Why it matters
module identity groupId:artifactId group:name groups all versions of the same logical module
version request <version>, range, SNAPSHOT fixed, dynamic/range, rich version starts version selection; may not equal final selected version
extra artifact type/classifier such as sources artifact/variant selection same module/version can expose different files or usages
resolved component mediated dependency node selected component + variant the node that contributes metadata/artifacts to a classpath

3. Metadata tells the resolver what a module means

The repository usually contains metadata separately from JAR bytes. A Maven POM can declare the module’s own dependencies, packaging, parent/BOM relationships, and other model information. Gradle can consume Maven POM metadata and, when available, Gradle Module Metadata with richer variant/attribute information. Resolution therefore has two conceptually separate jobs: construct the graph from metadata, then fetch the artifacts needed for the selected nodes/variants.

org/apache/commons/commons-text/1.10.0/
├── commons-text-1.10.0.pom       # metadata: includes dependency on commons-lang3 3.12.0
├── commons-text-1.10.0.jar       # executable/library bytes
├── commons-text-1.10.0-sources.jar
└── checksums/signatures ...

If metadata downloads successfully but the JAR is missing, dependency graph construction can appear to progress and artifact resolution can still fail. Treat those as different failure phases.

4. Direct declarations expand into a transitive graph

A direct dependency is declared by your project. A transitive dependency is reached through metadata from another dependency. For the lab, commons-text:1.10.0 declares commons-lang3:3.12.0. If the project also directly requests commons-lang3:3.9, the graph contains two version requests for the same module.

One graph, two requests for the same module
flowchart TD
  P["project"] --> T["commons-text 1.10.0"]
  P --> L39["commons-lang3 3.9
direct request"]
  T --> L312["commons-lang3 3.12.0
transitive request"]
  L39 -. "same module" .- L312
  L39 --> R["resolver chooses one version"]
  L312 --> R
  R --> C["resolved compile/runtime classpath"]

The resolved graph normally contains one selected version of a module for a given resolution context. The important question is not “which one did I declare?” but “which requests existed, which rule selected the winner, and where is that decision visible?”

5. Maven and Gradle use different default conflict models

Maven dependency mediation uses the nearest definition: the version reached by the shortest path from the project wins; when versions are at the same depth, declaration order can break the tie. A direct dependency is depth one, so the lab’s direct commons-lang3:3.9 request wins over the transitive 3.12.0 request unless management changes the result.

Gradle considers the version requests for the module across the graph and, by default, selects the highest compatible requested version according to Gradle’s version-ordering and constraint rules. In the same illustrative graph, Gradle therefore selects 3.12.0 unless a constraint, strict requirement, force/rule, platform, or another policy changes selection.

Resolver Requests in lab Default selected version Reason
Maven 3.9.x direct 3.9; transitive 3.12.0 3.9 nearest definition: direct path is nearer
Gradle 9.7.1 direct 3.9; transitive 3.12.0 3.12.0 default version conflict resolution selects the higher request

6. Fixed, ranged/dynamic, and changing identities answer different questions

A fixed version such as 1.10.0 asks for a named release. A range or dynamic selector asks the resolver to discover a currently acceptable version, which means repository metadata and time can change the answer. A Maven -SNAPSHOT or Gradle changing module is even more volatile: the same logical version coordinate can refer to different bytes over time.

Request style Example Can selected version change without source diff? Production posture
fixed release 1.10.0 not from selector alone; repository tampering is a separate risk preferred baseline; add verification/governance
Maven range / Gradle dynamic [1.0,2.0) or 1.+ yes, when new matching versions appear use only deliberately; lock/control when supported
snapshot/changing 1.0-SNAPSHOT yes, even coordinate can point to newer bytes development/integration only unless a strict policy justifies it

7. Local dependency state is a cache, not the authoritative project model

Maven’s local repository and Gradle’s dependency caches reduce repeated downloads. They can also hide repository outages, deleted upstream artifacts, or a changing-module update until refresh/expiry rules cause another lookup. That is why “it resolves on my laptop” is not proof that a clean CI agent can resolve the same graph.

Do not respond by deleting your normal ~/.m2/repository or ~/.gradle tree. Use an isolated Maven local repository or isolated GRADLE_USER_HOME for a clean-room check.

8. Inspect before changing the graph

./mvnw -v
./mvnw help:effective-pom
./mvnw help:effective-settings
./mvnw org.apache.maven.plugins:maven-dependency-plugin:3.10.0:tree
./gradlew -version
./gradlew dependencies --configuration runtimeClasspath
./gradlew dependencyInsight   --dependency commons-lang3   --configuration runtimeClasspath

These commands answer different questions: tool identity, effective model/repository policy, whole resolved graph, and why a particular Gradle module/version was selected. Capture this evidence before editing version numbers.

9. Why this matters in DevOps

CI/CD turns dependency resolution into a production input pipeline. If a build can silently select a new version, query an unexpected repository, or succeed only because a developer cache contains old bytes, downstream test and release evidence is ambiguous. A reliable delivery system records enough dependency identity and repository policy to explain exactly what entered the build.

Knowledge check

Your POM declares only commons-text, but commons-lang3 appears on the runtime classpath. What mechanism explains that?

A POM downloads successfully but the corresponding JAR returns 404. Is this a coordinate-selection failure or artifact-resolution failure?

Why can Maven and Gradle select different versions from the same two requests?

Does a fixed version by itself prove the downloaded JAR is trustworthy?

Why is a SNAPSHOT/changing module weaker reproducibility evidence than a fixed immutable release?

Summary

Dependency resolution starts with identity requests, expands through metadata into a graph, applies version/conflict and variant rules, and finally resolves artifact bytes from repositories/caches. Maven and Gradle share that pipeline but differ in important selection semantics. Treat the resolved graph and repository origin as observable build state, not invisible implementation detail.

Next lesson

Resolve a real conflict and observe both tools

Lesson 2 creates two disposable projects with the same dependency requests, captures the Maven tree and Gradle insight report, then repeats resolution with isolated dependency state.

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.