DevOpsInterviewPrep logo
← 🚀 Delivery & GitOps
Foundational

Maven and Gradle dependency resolution: explaining what actually entered the build

Compare Maven and Gradle dependency conflict rules, inspect the selected dependency graph, and connect version locking with artifact identity in CI.

TL;DR: A build file lists requested dependencies. The resolver decides which versions actually enter a classpath. When two paths request different versions of the same library, that decision can change runtime behavior even though the application source has not changed.

Consider this illustrative dependency graph. Both tools are resolving ordinary versions without additional constraints, substitutions, or custom rules:

rendering diagram…

Maven's usual mediation rule favors the nearest dependency definition, so the direct codec:1.2 wins in this example. Gradle's default version-conflict resolution ordinarily selects the highest requested version, so it chooses 1.4. Neither decision proves binary compatibility with every caller. Check the selected graph and run the compatibility tests that matter to the application.

Read the selection rule before changing a version

MechanismWhat it controlsWhat it does not prove
Maven dependency mediationSelection when dependency paths disagreeThat a selected older library has every required method
Maven dependencyManagementManaged versions and related dependency settingsThat a dependency has been added to the project
Gradle constraints or platformsPermitted or preferred versions under their configured rulesThat every artifact's bytes are trusted
Gradle dependency lockingResolved versions for locked configurationsThat repositories cannot serve different bytes for a version

At equal depth, Maven uses declaration order to settle the usual mediation tie. A parent or BOM can centralize versions through dependency management, but importing a BOM does not add all its libraries to the application. Consult the effective model when an inherited version is surprising. Maven's dependency mechanism explains these rules.

Gradle can also fail because of conflicting capabilities: two different components both claim to provide the same capability. That is a different problem from choosing between versions of one module. Avoid describing every resolution failure as “Gradle picked the newest.” Strict constraints, rejection rules, and enforced platforms can change the outcome. See Gradle conflict resolution.

Inspect a failure in CI

From an existing Maven project with its wrapper checked in, inspect the runtime graph:

./mvnw dependency:tree -Dscope=runtime -Dverbose

The dependency tree report helps identify the path that introduced a module and versions omitted during conflict resolution. Use the same profiles, settings, and repository configuration as the failing job. A laptop result from a different profile does not explain the CI classpath.

For a Gradle Java project, these wrapper commands examine the runtime classpath:

./gradlew dependencies --configuration runtimeClasspath
./gradlew dependencyInsight --dependency codec --configuration runtimeClasspath

codec is the illustrative module name above; replace it with the failing module. Other project types can use different configuration names. The dependency insight report explains selection reasons and the paths requesting a dependency.

Suppose a service starts failing with NoSuchMethodError after a parent-POM change. Compare the old and new resolved runtime graphs, then confirm which JAR the packaged application actually loads. Compilation may have succeeded against a different classpath, or a container layer may contain an older artifact. Clearing the cache before collecting that evidence destroys a useful comparison and may only make the failure temporarily disappear.

Locking is one input to reproducibility

Commit the build-tool wrapper and review its distribution identity. Record the JDK and plugin versions, and control dependency repositories. For Gradle, enable dependency locking for the configurations that are resolved in your workflow and commit the resulting lock state. Update locks deliberately with tests; a lock file cannot review its own compatibility change. Gradle dependency locking describes activation and update behavior.

Treat an artifact repository as part of the supply chain. A fixed version can still be unsafe if an authorized or compromised publisher replaces its bytes. Checksums or dependency verification, repository access controls, and retained build provenance address different parts of that problem. Avoid broad dynamic versions or changing artifacts in a release workflow whose result must be repeatable.

Once CI builds and tests an artifact, promote that same digest between environments. Rebuilding from the same Git commit can resolve a different graph or use different tools. The artifact promotion model keeps deployment identity tied to what passed the checks.

Try the reasoning

The graph above is moved from Maven to Gradle and the selected codec changes. Is that evidence of a broken cache? No. The default conflict rules already explain the change. Inspect the reports, decide the supported version explicitly, and test callers before changing cache behavior.

A locked build produces a different JAR hash on a second run. What remains to investigate? Compare dependency bytes, toolchains, generated timestamps, archive ordering, and other uncontrolled inputs. A matching dependency version list narrows the investigation; it does not establish byte-for-byte reproducibility.

RELATED CONCEPTS
PRACTICE THIS IN REAL QUESTIONS