Version Catalogs, Platforms, Constraints, Dependency Locking, and Centralized Version Governance: Guided Hands-On Workflow and Core Operations
Build a disposable multi-project Gradle workspace that uses a version catalog for aliases, a Java platform for policy, and lockfiles for resolved-state evidence, then perform a controlled upgrade.
Learning objectives
-
Create and consume a default
libs.versions.tomlcatalog without treating aliases as enforcement. -
Create a local
java-platformproject whose constraints provide versions to versionless catalog aliases. - Generate dependency lock state for two consuming subprojects and inspect the exact files produced.
-
Use
dependencyInsightto prove which constraint selected a version and how lock state participates. - Perform a controlled Lang 3.19.0 → 3.20.0 policy upgrade using targeted lock updates and a minimal diff.
- Explain which source-controlled governance files belong in review and which cache state must not be committed.
1. Lab outcome and safety boundary
You will create gradle-governance-lab with three
projects: :platform owns version policy,
:lib consumes Commons Lang, and
:app consumes :lib plus SLF4J. The catalog
owns dependency aliases; the platform owns external versions; each
consuming project locks its resolved state.
Wrapper rule: start from a disposable copy of a
project whose Gradle 9.7.1 Wrapper was already reviewed in Chapter
15. Do not generate a new wrapper from an unknown system Gradle
merely for this lab. Keep GRADLE_USER_HOME inside the
disposable lab and remove only that lab state during cleanup.
2. Preflight: record identity before changing files
cd gradle-governance-lab
export GRADLE_USER_HOME="$PWD/.gradle-user-home"
./gradlew --version
java -version
./gradlew projects
printf 'GRADLE_USER_HOME=%s\n' "$GRADLE_USER_HOME"
Expected baseline: Wrapper Gradle 9.7.1, a supported runtime JVM (the course uses JDK 21), and only disposable user-home state under this lab. If the Wrapper reports another version, stop and repair the wrapper baseline before teaching dependency governance.
3. Create the multi-project skeleton and centralized repository policy
// settings.gradle.kts
pluginManagement {
repositories { gradlePluginPortal() }
}
dependencyResolutionManagement {
repositories { mavenCentral() }
}
rootProject.name = "gradle-governance-lab"
include("platform", "lib", "app")
mkdir -p platform lib/src/main/java/dev/academy/lib app/src/main/java/dev/academy/app gradle
printf '%s\n' '# root build intentionally small' > README.lab.txt
dependencyResolutionManagement owns dependency
repository declarations for this lab. The version catalog, platform,
and lockfiles do not authenticate repositories or verify artifact
checksums; those remain separate supply-chain controls.
4. Create the version catalog as a coordinate layer
# gradle/libs.versions.toml
[libraries]
commons-lang3.module = "org.apache.commons:commons-lang3"
slf4j-api.module = "org.slf4j:slf4j-api"
Both aliases are deliberately versionless. Gradle supports module-only catalog entries. This makes the architecture visible: the catalog answers “which module?” while the platform answers “which version policy?”
5. Add a local Java platform with one strict and one normal constraint
// platform/build.gradle.kts
plugins { `java-platform` }
dependencies {
constraints {
api("org.apache.commons:commons-lang3") {
version { strictly("3.19.0") }
because("tested platform baseline for this lab")
}
api("org.slf4j:slf4j-api:2.0.17")
}
}
The Commons Lang constraint is intentionally strict so the later
out-of-policy example has a visible failure boundary. The SLF4J
shorthand is a required version, not a strict version. Neither
constraint adds a library to :lib or
:app by itself.
6. Consume the platform and catalog aliases
// lib/build.gradle.kts
plugins { `java-library` }
java {
toolchain { languageVersion = JavaLanguageVersion.of(17) }
}
dependencies {
api(platform(project(":platform")))
api(libs.commons.lang3)
}
dependencyLocking { lockAllConfigurations() }
// app/build.gradle.kts
plugins { application }
java {
toolchain { languageVersion = JavaLanguageVersion.of(17) }
}
dependencies {
implementation(platform(project(":platform")))
implementation(project(":lib"))
implementation(libs.slf4j.api)
}
application { mainClass = "dev.academy.app.App" }
dependencyLocking { lockAllConfigurations() }
// lib/src/main/java/dev/academy/lib/Words.java
package dev.academy.lib;
import org.apache.commons.lang3.StringUtils;
public final class Words {
private Words() {}
public static String normalize(String value) {
return StringUtils.capitalize(StringUtils.trim(value));
}
}
// app/src/main/java/dev/academy/app/App.java
package dev.academy.app;
import dev.academy.lib.Words;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class App {
private static final Logger LOG = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
LOG.info("normalized={}", Words.normalize(" governance "));
System.out.println(Words.normalize(" governance "));
}
}
Each consumer explicitly declares the platform dependency. The catalog alias adds the module edge; the platform supplies version policy. Locking is activated in the consumers so resolved external versions can become reviewed source state.
7. Inspect the model before writing lock state
./gradlew :lib:dependencies --configuration runtimeClasspath
./gradlew :lib:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath
./gradlew :app:dependencyInsight --dependency slf4j-api --configuration runtimeClasspath
Expected: Commons Lang resolves to 3.19.0 with a selection reason
involving the platform constraint; SLF4J resolves to 2.0.17. There
should be no lockfile yet in a fresh lab. Save these reports under
an evidence/ directory if you want immutable
before/after output.
8. Generate lock state deliberately
mkdir -p evidence
./gradlew :lib:dependencies :app:dependencies --write-locks | tee evidence/initial-lock-write.log
find lib app -name gradle.lockfile -type f -print
sed -n '1,120p' lib/gradle.lockfile
sed -n '1,160p' app/gradle.lockfile
cp lib/gradle.lockfile evidence/lib.before.lock
cp app/gradle.lockfile evidence/app.before.lock
Gradle only persists lock state for configurations it resolves
during the invocation. The dependencies tasks provide
an intentionally broad read of each project’s resolvable
configurations. The resulting lockfiles are source-controlled
governance artifacts; .gradle-user-home/, project
.gradle/, and build/ remain
generated/cache state.
9. Controlled upgrade: change policy first, then update only the target lock
Edit only the Commons Lang constraint in
platform/build.gradle.kts:
version { strictly("3.20.0") }
Prediction before running: the existing lockfile still pins 3.19.0, while the platform now strictly accepts only 3.20.0. Normal resolution should therefore fail until lock state is intentionally updated.
set +e
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath > evidence/stale-lock.txt 2>&1
status=$?
set -e
printf 'stale-lock status=%s\n' "$status"
sed -n '1,160p' evidence/stale-lock.txt
./gradlew :lib:dependencies :app:dependencies --update-locks org.apache.commons:commons-lang3 | tee evidence/targeted-update.log
diff -u evidence/lib.before.lock lib/gradle.lockfile || true
diff -u evidence/app.before.lock app/gradle.lockfile || true
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath
The targeted update keeps the existing lock state as input except for the requested module. Gradle warns that graph rules can still cause related modules to change, so “targeted” means narrow intent, not a guarantee that exactly one physical line must change. Review the actual lock diff.
10. Build and verify causality
./gradlew clean :lib:build :app:build :app:run
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath
sha256sum lib/gradle.lockfile app/gradle.lockfile > evidence/lockfile-sha256.txt
cat evidence/lockfile-sha256.txt
The build proves the selected graph is usable;
dependencyInsight proves why Lang 3.20.0 was selected;
the lockfile diff proves the resolved-state capture changed only
under explicit review.
11. Challenge: choose the correct control
A teammate asks to “make SLF4J 2.0.17 mandatory everywhere” and
proposes editing only libs.versions.toml. Which layer
should you change?
Choose before revealing: if the requirement is graph policy, put it in the platform/constraint model (with an appropriate strength) and then regenerate/update lock state if selected versions change. A catalog-only change improves declaration consistency but does not itself enforce resolution.
12. Cleanup and source-control policy
# Review before cleanup
printf 'Commit candidates:\n'
printf ' gradle/libs.versions.toml\n platform/build.gradle.kts\n lib/build.gradle.kts\n app/build.gradle.kts\n lib/gradle.lockfile\n app/gradle.lockfile\n'
cd ..
rm -rf gradle-governance-lab
In a real repository, also commit the trusted Wrapper files and
source code. Do not commit GRADLE_USER_HOME, project
.gradle/, downloaded artifacts, or credentials. This
cleanup deletes only the disposable lab tree.
Knowledge check
Why are the catalog aliases versionless in this lab?
To make responsibilities explicit: the catalog centralizes coordinates/accessors while the Java platform owns version policy.
Why does changing the platform from strict 3.19.0 to strict 3.20.0 fail against the old lock?
Gradle applies lock state like strict version constraints. The old lock insists on 3.19.0 while the new platform strictly accepts 3.20.0.
What does
--update-locks org.apache.commons:commons-lang3 do
differently from broad --write-locks?
It retains existing lock state as input while relaxing the specified module(s) for update, reducing the intended review surface.
Should .gradle-user-home/caches be committed with
the lockfile?
No. It is machine-local cache state. The lockfile is the source-controlled resolved-version record.
If a targeted update changes another transitive module too, is Gradle necessarily wrong?
No. Gradle documents that normal resolution rules may require related module updates; review the causal graph and diff instead of assuming one-line output.
Which evidence proves why a version won?
dependencyInsight for the relevant resolvable
configuration, combined with the platform/constraint and
lockfile state.
Official references and version notes
- Gradle Version Catalogs — alias/accessor semantics, TOML format, rich versions, publishing/sharing.
- Using Catalogs with Platforms — explains why catalogs do not enforce graph versions and how platforms differ.
- Gradle Platforms and Java Platform Plugin — constraints, BOM imports, regular versus enforced platforms.
-
Declaring Versions and Ranges
—
require,strictly,prefer, andreject. -
Dependency Locking
— activation,
--write-locks,--update-locks, lockfile location, and lock modes. - Viewing and Debugging Dependencies — dependency reports and selection reasons.
- Gradle 9.7.1 Release Notes — pinned chapter baseline.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.