Chapter 20Lesson 04~200 minutes

Version Catalogs, Platforms, Constraints, Dependency Locking, and Centralized Version Governance: Diagnostics, Failure Modes, Security, and Performance

Diagnose stale lock state, accidental broad updates, dynamic selectors, conflicting strict constraints, and divergent subproject policy without hiding evidence or deleting normal caches.

DiagnosticsStale locksDynamic versionsStrict conflictsSupply chain

Learning objectives

  • Diagnose a version-catalog expectation that differs from the resolved graph without assuming the catalog is broken.
  • Interpret stale lock failures and distinguish intentional targeted updates from accidental broad lock regeneration.
  • Identify why dynamic selectors and changing modules weaken review expectations even when locking is present.
  • Repair incompatible strict constraints using graph evidence rather than force/exclusion guesswork.
  • Detect subprojects that have escaped centralized platform/catalog policy.
  • Use isolated caches and concise diagnostics without deleting normal Gradle state or leaking sensitive settings.

1. Evidence-first diagnostic sequence

Use the same production sequence established earlier in the course:

  1. Preserve concise failure output and lock/catalog/platform diffs.
  2. Confirm Wrapper Gradle and runtime JDK identity.
  3. Inspect declared catalog/platform/build configuration.
  4. Inspect the relevant resolvable configuration with dependencies/dependencyInsight.
  5. Inspect lockfiles and repository/cache state without mutating them.
  6. Interpret the constraint/lock conflict or resolution message.
  7. Apply the narrowest correction.
  8. Re-resolve in a controlled run and verify the lock diff plus build result.

2. Failure: “the catalog says 3.19.0, so why did 3.20.0 win?”

This is not necessarily a failure at all. A catalog version creates a dependency request. Another direct/transitive request or platform constraint may legitimately select 3.20.0.

./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath

Read the selection reasons. “By conflict resolution,” “By constraint,” “Forced,” or lock-related failures tell you which policy layer actually changed the result. Repair the policy only if the selected result violates documented compatibility—not merely because it differs from the TOML text.

3. Failure: platform upgraded, lockfile still captures the old version

With a strict platform constraint at 3.20.0 and a lock at 3.19.0, resolution can become unsatisfiable because both are strict opinions. Preserve that failure: it proves the lock is preventing silent drift.

./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath --stacktrace
# after review of the policy change:
./gradlew :app:dependencies --update-locks org.apache.commons:commons-lang3

Do not solve this by deleting the lockfile and running a broad build. Update the specific module after reviewing the policy change, then inspect all resulting lock diffs.

4. Failure: a broad lock update changes far more than expected

--write-locks overwrites lock state for configurations resolved in that invocation. If someone runs it after changing multiple selectors, many entries can move. Preserve the old lockfiles, compare them, and reproduce selection with dependencyInsight. If the intended change was one module, restore the unrelated lock state and retry with --update-locks group:module.

CI policy: ordinary verification pipelines should not include --write-locks. A successful build that silently mutates source governance is not a verification build.

5. Failure: a dynamic selector defeats human review expectations

[libraries]
# Diagnostic anti-pattern for production governance:
lang3-dynamic = { module = "org.apache.commons:commons-lang3", version = "3.+" }

Dependency locking can stabilize the selected version after resolution, but the declaration still says “choose something from a moving set.” That can be deliberate for an exploratory lane, yet it weakens review intent and requires disciplined lock updates. Changing versions (for example snapshots whose bytes can change under the same coordinates) are worse: a version lock cannot make the content immutable.

6. Intentionally broken example: incompatible strict constraints

Start from a platform that strictly accepts 3.20.0, then temporarily add this direct dependency in :app:

dependencies {
    implementation("org.apache.commons:commons-lang3") {
        version { strictly("3.18.0") }
        because("intentional Chapter 20 conflict")
    }
}
set +e
./gradlew :app:dependencies --configuration runtimeClasspath > evidence/strict-conflict.txt 2>&1
status=$?
set -e
printf 'strict-conflict status=%s\n' "$status"
sed -n '1,200p' evidence/strict-conflict.txt

Expected: resolution fails because no version can satisfy both strict opinions. Do not add an exclusion, force, or cache deletion. Remove the accidental direct strict constraint—or change the platform policy only if the compatibility requirement itself was wrong—then rerun insight and the build.

7. Failure: one subproject is outside the platform policy

A multi-project build can appear centralized while one subproject declares implementation(libs.commons.lang3) without also consuming platform(project(":platform")). Compare subproject dependency reports and inspect build scripts. The narrow fix is to apply the platform consistently—preferably through reviewed convention logic—rather than duplicate versions into the stray subproject.

8. Cache/repository diagnostic: prove the model before touching normal state

# Keep the normal user cache untouched.
export GRADLE_USER_HOME="$PWD/.fresh-gradle-home"
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath --refresh-dependencies

A fresh user home separates repository/download state from the committed catalog/platform/lock model. It can be slower and may require network access, so use it only when cache/repository state is actually in question. Never recommend recursively deleting the user’s normal ~/.gradle as the first diagnostic step.

9. Security and log boundaries

Catalog/platform/lockfiles are usually safe to commit, but dependency repositories may require credentials and may expose private package names. Debug logs can reveal repository URLs, environment details, and server configuration. Keep credentials in Gradle’s supported credential/property mechanisms, use fake values in labs, and do not paste full --debug output into public tickets without review. Version governance is not a substitute for dependency verification; Chapter 28 handles checksum/signature policy.

10. Performance: measure the phase that changed

A cold isolated home measures dependency download/metadata cost as well as graph resolution; a warm run measures mostly model and graph work. Locking can reduce version-selection ambiguity but does not remove repository access for missing artifacts. Do not attribute a slow cold build to “lockfiles” unless timings and logs show lock processing is the cause.

Knowledge check

A catalog requests 3.19.0 and insight selects 3.20.0 by constraint. Is the catalog corrupt?

Why is a stale-lock failure after a reviewed platform upgrade useful?

Why not delete the lockfile to fix a stale lock?

What does a conflict between strict 3.20.0 and strict 3.18.0 mean?

Does locking make a changing/SNAPSHOT module immutable?

When should you try an isolated Gradle User Home?

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.