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.
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.
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.
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.
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?
Transitivity: commons-text metadata declares commons-lang3, so the resolver adds that edge to the graph.
A POM downloads successfully but the corresponding JAR returns 404. Is this a coordinate-selection failure or artifact-resolution failure?
The metadata/graph phase has progressed, but artifact resolution failed. Diagnose repository origin and artifact availability rather than changing unrelated source code.
Why can Maven and Gradle select different versions from the same two requests?
Their default conflict rules differ: Maven uses nearest-definition mediation; Gradle normally selects the highest requested compatible version.
Does a fixed version by itself prove the downloaded JAR is trustworthy?
No. It improves identity stability, but repository trust, checksums/signatures/verification, provenance, and vulnerability status are separate controls.
Why is a SNAPSHOT/changing module weaker reproducibility evidence than a fixed immutable release?
The same logical version can be republished with different metadata or bytes, so identical source/configuration may resolve different content over time.
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.
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.