ExerciseChallenge
Challenge
Resolve six conflicting trees
25 minintermediate1–15 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- Nearest-wins and highest-wins give different answers for the same tree
- Version comparison is numeric per component, never lexicographic
- dependencyManagement overrides depth entirely, which is why it is the right pinning tool
- Some conflicts have no version that satisfies everyone, and saying so is the answer
Starter
Starter.java
import java.util.*;
/**
* Challenge: resolve six dependency trees two different ways.
*
* You are implementing the part of a build tool that answers "which version
* ends up on the classpath". Two rules, six trees, and one tree that has no
* correct answer at all.
*/
public class Starter {
/** One reachable version of one artifact: how deep, and through what path. */
record Dep(String artifact, String version, int depth, String via) {}
/** A tree is just the set of (artifact, version, depth) edges it produces. */
record Tree(String name, List<Dep> edges, String note) {}
static final List<Tree> TREES = List.of(
// 1. The textbook case: newer version is further away.
new Tree("basic", List.of(
new Dep("commons-lang3", "3.4", 2, "report-service"),
new Dep("commons-lang3", "3.12", 3, "audit-client -> http-toolkit")),
"which rule downgrades you?"),
// 2. Same depth. Now what decides?
new Tree("tie", List.of(
new Dep("guava", "31.0", 2, "search-client (declared first)"),
new Dep("guava", "32.1", 2, "billing-client (declared second)")),
"Maven breaks ties by declaration order"),
// 3. Lexicographic ordering is wrong here.
new Tree("double-digit", List.of(
new Dep("jackson-databind", "2.9.0", 2, "legacy-api"),
new Dep("jackson-databind", "2.17.1", 3, "spring-boot-starter-web")),
"sort these as strings and see what you get"),
// 4. Three paths, three versions.
new Tree("three-way", List.of(
new Dep("slf4j-api", "1.7.36", 4, "old-batch -> shared-core -> logging-shim"),
new Dep("slf4j-api", "2.0.7", 2, "http-client"),
new Dep("slf4j-api", "2.0.13", 3, "metrics-agent -> exporter")),
"the rules disagree, and neither picks the middle one"),
// 5. The root declares it directly.
new Tree("root-pinned", List.of(
new Dep("commons-io", "2.11.0", 1, "declared in this pom"),
new Dep("commons-io", "2.15.1", 2, "storage-client")),
"depth 1 beats everything under nearest-wins"),
// 6. No version satisfies everyone.
new Tree("unsatisfiable", List.of(
new Dep("crypto-utils", "3.9", 2, "signing-service (calls sign(byte[]) — REMOVED in 3.10)"),
new Dep("crypto-utils", "3.12", 2, "token-service (calls verify(Key) — ADDED in 3.12)")),
"read the callers, not just the numbers")
);
public static void main(String[] args) {
// TODO 1: write compareVersions(String, String) comparing numeric
// components, so 1.10.0 > 1.9.0. Prove it against String.compareTo
// before you use it anywhere.
// TODO 2: implement nearestWins — the smallest depth. Decide what
// happens on a tie and say which tree that matters for.
// TODO 3: implement highestWins — the greatest version, ignoring depth.
// TODO 4: run all six trees through both rules and print a table:
// tree name, Maven's answer, Gradle's answer, and whether they agree.
// TODO 5: for every tree where they disagree, say in one line what a
// developer would experience — specifically, who breaks and when.
// TODO 6: model dependencyManagement. It is not another candidate; it
// replaces the outcome regardless of depth. Add:
//
// Map<String, String> managed = Map.of("commons-lang3", "3.12");
//
// and make both rules consult it first. Re-run tree 1.
// TODO 7: tree 6 has no version that works. Detect it — the notes tell
// you which methods each caller needs. Report it as unsatisfiable
// rather than picking one, and list the three real options.
// TODO 8: which single tree would you most want the build to FAIL on
// rather than resolve silently? Say why, and name the Maven and Gradle
// settings that would do it.
}
}Run it locally:
cd exercises/java/dependencies/transitive-conflicts/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Implement both rules before looking at any tree. Then run every tree through both and compare — the disagreements are the interesting cases.
Hint 2
Sort 1.9.0, 1.10.0 and 1.10.2 with String.compareTo, then with your comparator. One of the six trees is designed around this.
Hint 3
dependencyManagement is not another candidate in the list. It replaces the choice, at any depth. Model it as a lookup consulted first.
Hint 4
One tree has no winner: a caller needs a method added in 3.12 and another needs one removed in 3.10. Say so and name the three options.
Done when
- Both resolution rules implemented and run over all six trees
- A table showing which trees the two rules disagree on
- A numeric version comparator, with a test that lexicographic ordering fails
- dependencyManagement modelled as an override that ignores depth
- The unsatisfiable tree identified, with the options named rather than a version chosen
← Back to Two libraries need different versions of the same dependency. What happens?