Chapter 20Lesson 01~190 minutes

Version Catalogs, Platforms, Constraints, Dependency Locking, and Centralized Version Governance: Concepts, Architecture, and Mental Model

Separate dependency declaration ergonomics from graph policy and resolved-state capture so a central version file never becomes a false promise of reproducibility.

Gradle 9.7.1Version catalogsPlatformsConstraintsLockfiles

Learning objectives

  • Distinguish version-catalog aliases, platform/constraint policy, and dependency lock state by the exact build state each mechanism controls.
  • Explain why a version listed in libs.versions.toml is a requested version, not an immutable resolution result.
  • Compare regular platforms, strict constraints, preferred/rejected versions, and enforced platforms without treating them as interchangeable.
  • Explain how Gradle dependency locking captures selected module versions and how lock state participates in later resolution.
  • Identify the source-controlled governance files, machine-local caches, repository metadata, and CI evidence that must remain separate.
  • Inspect dependency intent and resolved selection before changing governance policy.

1. The practical problem: one “version file” cannot do four jobs

Chapter 19 showed that dependency resolution is a graph-and-variant decision, not a string lookup. Chapter 20 adds governance. Teams often begin by centralizing coordinates in one file and then assume the build is “locked.” That assumption is dangerous because four different questions are being collapsed:

  • How do authors spell a dependency? A version catalog can provide aliases such as libs.commons.lang3.
  • What versions are acceptable or recommended? Platforms and constraints participate in resolution policy.
  • What version did this build actually select? Dependency reports and dependencyInsight show the graph decision.
  • What exact selection must subsequent builds reproduce? Dependency locking records resolved module versions in lock state.

Chapter invariant: declaration convenience, resolution policy, and resolved-state capture are separate layers. A review should be able to point to the file/evidence for each layer.

2. Baseline and terminology

The mandatory path uses Gradle 9.7.1 through the project Wrapper, JDK 21 to run Gradle, Java 17 as the course target, Maven Central only for public dependency examples, and a disposable GRADLE_USER_HOME. No external plugin is required.

Object Beginner-safe meaning Typical evidence
Version catalog A named catalog of aliases, coordinates, versions, bundles, and plugin aliases. The default file is gradle/libs.versions.toml. TOML + generated libs.* accessors.
Platform A dependency component that contributes dependency constraints to the graph. A java-platform project can publish Gradle metadata and a Maven BOM. Platform build script + dependency graph.
Constraint A version requirement that participates only if the module is present; it does not add the module by itself. Platform/build script + insight selection reasons.
Rich version A version expression using require, strictly, prefer, and/or reject. Declared constraint and dependency insight.
Lock state A source-controlled record of resolved module versions for configurations where locking is active. Per-project gradle.lockfile.
Dependency verification Integrity/provenance checking for artifacts and metadata. It is not dependency locking. Covered later in Chapter 28; do not conflate it with versions.

3. Four governance layers and their data flow

Declared intent to reproducible selection
flowchart TD
    C["libs.versions.toml aliases"] --> D["Dependency declarations"]
    P["Platform + constraints"] --> R["Resolution engine"]
    D --> R
    M["Repository metadata"] --> R
    L["Existing gradle.lockfile"] --> R
    R --> S["Selected graph"]
    S --> W["--write-locks / --update-locks"]
    W --> L2["Reviewed lock state"]
    S --> A["Artifact files in cache"]
  

The catalog supplies convenient dependency notation. The dependency declaration and platform constraints become graph inputs. Repository metadata contributes transitive requirements. Existing lock state constrains resolution. The selected graph can then be persisted as updated lock state. Artifact bytes are downloaded into the Gradle dependency cache, which is machine-local state and is not the governance source of truth.

4. Version catalogs centralize declaration intent, not final selection

Gradle automatically recognizes gradle/libs.versions.toml and exposes type-safe libs accessors. A catalog entry can carry an explicit version, a rich version, or only a module coordinate. But a catalog request enters ordinary resolution: another dependency, a constraint, a platform, or lock state can change the selected version.

[versions]
lang3 = "3.19.0"

[libraries]
commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "lang3" }
slf4j-api.module = "org.slf4j:slf4j-api"
dependencies {
    implementation(libs.commons.lang3)
    implementation(libs.slf4j.api) // intentionally versionless: a platform can supply it
}

If a catalog requests Lang 3.19.0 and a compatible graph constraint selects 3.20.0, Gradle can select 3.20.0. That is why the official Gradle documentation explicitly says catalogs do not enforce versions during conflict resolution.

5. Platforms and constraints are graph policy

The java-platform plugin creates a component whose primary payload is dependency constraints rather than Java classes. Consumers add it with platform(project(":platform")) or platform("group:bom:version"). The platform then participates in graph selection, including transitive dependencies.

plugins { `java-platform` }

dependencies {
    constraints {
        api("org.apache.commons:commons-lang3") {
            version { strictly("3.19.0") }
            because("validated platform baseline")
        }
        api("org.slf4j:slf4j-api:2.0.17")
    }
}

A regular platform(...) preserves ordinary conflict semantics while importing constraints. enforcedPlatform(...) overrides other version requests and exports that force transitively, so Gradle recommends caution—especially for reusable libraries. Prefer explicit constraints where consumers need room to negotiate compatibility.

6. Rich versions express different strengths of opinion

Term Meaning in Gradle resolution Typical governance use
require The selected version must satisfy the requirement, but a higher compatible conflict winner can still be chosen. Ordinary shorthand versions are required versions. Minimum/range policy where compatible upgrades are acceptable.
strictly The strongest declaration. Versions outside the strict expression are excluded; incompatible strict opinions can make resolution fail. A tested compatibility boundary or explicit downgrade/freeze.
prefer A soft preference used only when no stronger non-dynamic opinion determines the result. Suggest a known-good version inside an allowed range.
reject Marks specific versions/ranges unacceptable. Exclude known-bad releases while preserving flexibility elsewhere.

Strictness is not automatically “better.” A library that exports a strict constraint can restrict downstream consumers. Governance should document why the strength is justified and how a consumer can diagnose incompatibility.

7. Dependency locking captures selected state

Locking is activated per resolvable configuration or broadly with lockAllConfigurations(). After activation, running a resolution command with --write-locks creates or updates gradle.lockfile. Gradle documents lock state as source-controlled input and applies locked versions like strict constraints during later resolution.

dependencyLocking {
    lockAllConfigurations()
}
./gradlew :app:dependencies --write-locks
sed -n '1,120p' app/gradle.lockfile

Two cautions matter. First, only configurations actually resolved during the command can contribute lock state; “locking enabled” does not mean every possible configuration was resolved. Second, locks capture versions, not immutable artifact contents. A changing module such as a SNAPSHOT can keep the same coordinates while its bytes change, which is why Gradle warns against treating dependency locking as a solution for changing dependencies.

8. Read-only inspection before mutation

Before editing the catalog, platform, or lockfiles, establish the current model and resolution result:

./gradlew --version
java -version
./gradlew projects
./gradlew :app:dependencies --configuration runtimeClasspath
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath
sed -n '1,160p' gradle/libs.versions.toml
find . -name gradle.lockfile -type f -print

The first five commands identify the tool/JVM, project graph, resolved dependency graph, and selection reasons. The final two show declaration governance and captured lock state without regenerating anything.

9. Files, caches, repositories, and trust boundaries

State Typical location Authoritative for Not authoritative for
Catalog gradle/libs.versions.toml Aliases/requested versions Final resolved graph
Platform policy platform/build.gradle.kts or imported BOM Constraints/alignment policy Which dependencies are actually declared
Lock state */gradle.lockfile Previously selected module versions Artifact byte integrity
Build declarations build.gradle(.kts) Which dependencies/platforms/configurations are connected Repository artifact authenticity
Dependency cache GRADLE_USER_HOME/caches Reusable local resolution/artifact state Release identity or governance
Remote repository Maven/Ivy repository Published metadata/artifacts Your organization’s accepted policy by itself

Catalog and lockfile changes are code-review material. Repository credentials, dependency-verification metadata, plugin repositories, and wrapper checksums are separate security-sensitive boundaries; this chapter does not hide them inside “version management.”

10. Common wrong mental models

  • “The catalog pins everything.” No: it creates dependency requests/accessors.
  • “A constraint adds a dependency.” No: it only influences the module if that module enters the graph.
  • “A platform and a lockfile are interchangeable.” No: the platform declares policy; the lockfile captures a resolved state.
  • “enforcedPlatform is simply a stronger BOM.” It exports force-like behavior transitively and can be hostile to reusable-library consumers.
  • “A lockfile proves the JAR is trustworthy.” No: integrity verification is a different control.

11. DevOps operating model

A production dependency-governance review should preserve at least four kinds of evidence: the declaration diff (catalog/build files), the policy diff (platform/constraints), the resolved graph/selection reasons, and the lockfile diff. A bot that changes all four in one opaque commit defeats the auditability centralization was supposed to create. Chapter 20 therefore treats “update dependency” as a controlled state transition rather than a search-and-replace.

Knowledge check

Why can libs.versions.toml say 3.19.0 while dependencyInsight reports 3.20.0?

Does a platform constraint on module X add X to the graph?

What does a gradle.lockfile capture?

When is enforcedPlatform risky?

Which is stronger: prefer or strictly?

A CI build has a warm cache and passes. Does that prove lock governance works from a clean agent?

Official references and version notes

Version-sensitive statements were rechecked for Gradle 9.7.1 on 2026-08-24. This chapter does not depend on Maven 4, a third-party Gradle plugin, hosted CI, or a commercial repository.

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.