---
title: 'Dependency Management'
source: 'https://academia.sh/en/courses/java-ecosystem/dependency-management'
course: 'Build, Testing and the Ecosystem'
language: en
updated: '2026-09-13T05:08:57+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures which class path a dependency is required on by actually separating the two paths'
    - 'Shows that a transitive dependency can be required at run time without ever appearing on the compile class path'
    - 'Tests that when two implementations of the same interface are both present on the runtime class path, the choice depends on order'
    - 'Counts compile, runtime, and silent defect classes by separating them from each other'
---

# Dependency Management

The same source is first compiled against a compile class path and then run against a separate runtime class path: of six types, 1 turns out required only at compile time, 3 only at run time, 2 at both. Of four defects, 1 is a compile error, 1 is a runtime error, and 2 silently produce a wrong result without saying anything — one a choice tied to the order of two implementations of the same interface, the other the price of changing a constant without recompiling the caller.

The previous lesson showed which class path a dependency is required on with two extreme
examples: one required only at compile time, one only at run time. But a real declaration
usually also carries dependencies sitting in between the two — required at both compile time
and run time, or never appearing at compile time and leaking into the run only through another
dependency. This lesson puts six dependency types side by side and asks all of them the same
question: which class path must this type be present on? The answer cannot be extracted from
the source text — if it could, there would be no need for a concept called dependency scope at
all.

## Dependency Scope: Three Values, One Question

A dependency's **dependency scope** takes one of three values: required only on the compile
class path, required only on the runtime class path, or required on both. The scope is not a
guess, it is a decision written in the declaration — but whether that decision is correct can
only be tested by actually separating the two paths. The six types below give six different
answers to the same question, and none of them can be extracted from a surface reading of the
source text:

- `Constant` — a class carrying a compile-time constant.
- `Tool` — a helper class that calls `Deep` directly, not `Constant`.
- `Deep` — a class only `Tool` calls, never named directly by `User`.
- `Facade` — a single-method interface.
- `ProviderA`, `ProviderB` — two separate implementations of `Facade`; they are present through
  a service declaration without ever being named in the source text.

- **BT7.** The class path requirement of the six types is measured by first compiling the same
  source against a compile class path and then running it against a separate runtime class
  path; a trial done with only one path does not show this distinction.
- **BT8.** `Constant` being a compile-time constant (`static final int`) comes from the same
  source as the value-embedding mechanism established in the Object-Oriented Java course; it is
  not re-established here, it is only used as a tool for the class path result.
- **BT9.** `Facade` is an interface; `ProviderA` and `ProviderB` are bound to it through a
  service declaration (under `META-INF/services`). `Caller` never writes any implementation
  name in its source text at all, it only looks for `Facade`.

```java
// ClassPath.java — a helper that counts which class path a type is required on and where a divergence is caught
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class ClassPath {
    static int compile = 0, runtime = 0, both = 0;
    static int compileError = 0, runtimeError = 0, silent = 0;

    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, Path source) throws IOException {
        Files.createDirectories(output);
        List<String> arg = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        arg.add(source.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silentOutput = new StringWriter();
        int result = compiler.run(new PrintWriter(silentOutput), new PrintWriter(silentOutput), arg.toArray(new String[0]));
        return result == 0;
    }

    static String run(List<Path> classPath, String mainClass) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, ClassPath.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return "ok";
        } catch (java.lang.reflect.InvocationTargetException e) {
            return e.getCause().getClass().getSimpleName();
        } catch (Throwable t) {
            return t.getClass().getSimpleName();
        }
    }

    static void declareProvider(Path dir, String provider) throws IOException {
        Path d = dir.resolve("META-INF").resolve("services");
        Files.createDirectories(d);
        Files.writeString(d.resolve("Facade"), provider + "\n");
    }

    static String selectedProvider(List<Path> classPath) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, ClassPath.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName("Caller", true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return (String) cls.getField("selected").get(null);
        }
    }

    static int readField(List<Path> classPath, String mainClass, String field) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, ClassPath.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return cls.getField(field).getInt(null);
        }
    }

    static void classify(String name, boolean compileRequired, boolean runtimeRequired) {
        String where;
        if (compileRequired && runtimeRequired) { both++; where = "both"; }
        else if (compileRequired) { compile++; where = "compile"; }
        else { runtime++; where = "runtime"; }
        System.out.printf("%-32s %s%n", name, where);
    }

    static void defect(String name, String kind) {
        if (kind.equals("compile")) compileError++;
        else if (kind.equals("runtime")) runtimeError++;
        else silent++;
        System.out.printf("%-32s %s%n", name, kind);
    }

    static void summary() {
        System.out.printf("type   : %d compile only, %d runtime only, %d both%n", compile, runtime, both);
        System.out.printf("defect : %d compile error, %d runtime error, %d silent%n", compileError, runtimeError, silent);
    }
}
```

`ClassPath` gathers three separate measurements under one roof: which class path a type is
required on (`classify`), which stage a defect falls at (`defect`), and a total of both
(`summary`). `compile` and `run` do the same operations as in the previous lesson;
`declareProvider` writes a service declaration to disk, and `selectedProvider` runs `Caller` and
reads which implementation was selected.

## Six Types, One Measurement

- **BT10.** The transitive dependency (`Deep`) is measured never entering the compile class
  path at all, even though the direct dependency (`Tool`) is present on it; the source
  compiling is not proof of this.
- **BT11.** Conflict resolution is measured by putting two implementations of the same
  interface (`ProviderA`, `ProviderB`) on the runtime class path at the same time and swapping
  their order; which order they are given in is the only variable.

```java
// DependencyMeasurement.java — measures which class path six types are required on and where four defects appear
import java.nio.file.*;
import java.util.*;

public class DependencyMeasurement {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("dependency");
        Path source = root.resolve("source");
        Path constantD = root.resolve("constant"), toolD = root.resolve("tool"), deepD = root.resolve("deep");
        Path facadeD = root.resolve("facade"), aD = root.resolve("providerA"), bD = root.resolve("providerB");
        Path userD = root.resolve("user"), callerD = root.resolve("caller");

        Path sConstant = ClassPath.write(source, "Constant", "public class Constant { public static final int VALUE = 7; }");
        Path sDeep = ClassPath.write(source, "Deep", "public class Deep { public static int three() { return 3; } }");
        Path sTool = ClassPath.write(source, "Tool", "public class Tool { public static int two() { return Deep.three() - 1; } }");
        Path sUser = ClassPath.write(source, "User",
                "public class User { public static void main(String[] a) { int t = Constant.VALUE + Tool.two(); if (t < 0) throw new IllegalStateException(); } }");
        Path sFacade = ClassPath.write(source, "Facade", "public interface Facade { String name(); }");
        Path sA = ClassPath.write(source, "ProviderA", "public class ProviderA implements Facade { public String name() { return \"A\"; } }");
        Path sB = ClassPath.write(source, "ProviderB", "public class ProviderB implements Facade { public String name() { return \"B\"; } }");
        Path sCaller = ClassPath.write(source, "Caller",
                "import java.util.ServiceLoader;\n"
              + "public class Caller {\n"
              + "  public static String selected = \"none\";\n"
              + "  public static void main(String[] a) {\n"
              + "    for (Facade f : ServiceLoader.load(Facade.class, Caller.class.getClassLoader())) { selected = f.name(); return; }\n"
              + "  }\n"
              + "}");

        ClassPath.compile(constantD, List.of(), sConstant);
        ClassPath.compile(deepD, List.of(), sDeep);
        ClassPath.compile(toolD, List.of(deepD), sTool);
        ClassPath.compile(facadeD, List.of(), sFacade);
        ClassPath.compile(aD, List.of(facadeD), sA);
        ClassPath.compile(bD, List.of(facadeD), sB);
        ClassPath.declareProvider(aD, "ProviderA");
        ClassPath.declareProvider(bD, "ProviderB");
        ClassPath.compile(userD, List.of(constantD, toolD), sUser);
        ClassPath.compile(callerD, List.of(facadeD), sCaller);

        System.out.println("-- six types, which class path each is required on --");

        boolean constantCompile = !ClassPath.compile(root.resolve("d1"), List.of(toolD), sUser);
        boolean constantRuntime = !ClassPath.run(List.of(userD, toolD, deepD), "User").equals("ok");
        ClassPath.classify("Constant (compile-time constant)", constantCompile, constantRuntime);

        boolean toolCompile = !ClassPath.compile(root.resolve("d2"), List.of(constantD), sUser);
        boolean toolRuntime = !ClassPath.run(List.of(userD, constantD, deepD), "User").equals("ok");
        ClassPath.classify("Tool (direct dependency)", toolCompile, toolRuntime);

        boolean deepCompile = !ClassPath.compile(root.resolve("d3"), List.of(constantD, toolD), sUser);
        boolean deepRuntime = !ClassPath.run(List.of(userD, constantD, toolD), "User").equals("ok");
        ClassPath.classify("Deep (transitive dependency)", deepCompile, deepRuntime);

        boolean facadeCompile = !ClassPath.compile(root.resolve("d4"), List.of(), sCaller);
        boolean facadeRuntime = !ClassPath.run(List.of(callerD, aD), "Caller").equals("ok");
        ClassPath.classify("Facade (interface)", facadeCompile, facadeRuntime);

        boolean aCompile = !ClassPath.compile(root.resolve("d5"), List.of(facadeD), sCaller);
        ClassPath.classify("ProviderA (implementation)", aCompile, true);
        ClassPath.classify("ProviderB (second implementation)", aCompile, true);

        System.out.println();
        System.out.println("-- transitive dependency: Deep never enters the compile class path via Tool --");
        System.out.println("did User compile against the [Constant, Tool] compile class path: " + ClassPath.compile(root.resolve("transit"), List.of(constantD, toolD), sUser));
        System.out.println("Deep is not on this compile class path at all, yet User compiles");

        System.out.println();
        System.out.println("-- conflict resolution: two implementations on the runtime path, order decides the choice --");
        String firstAB = ClassPath.selectedProvider(List.of(callerD, facadeD, aD, bD));
        String firstBA = ClassPath.selectedProvider(List.of(callerD, facadeD, bD, aD));
        System.out.println("selected when A is given first: " + firstAB);
        System.out.println("selected when B is given first: " + firstBA);

        System.out.println();
        System.out.println("-- where the defect appears once the paths diverge --");
        ClassPath.defect("type absent on the compile path", ClassPath.compile(root.resolve("d6"), List.of(constantD), sUser) ? "silent" : "compile");
        ClassPath.defect("type absent on the runtime path", ClassPath.run(List.of(userD, constantD), "User").equals("ok") ? "silent" : "runtime");
        ClassPath.defect("two implementations on the runtime path", firstAB.equals(firstBA) ? "runtime" : "silent");

        Path constantNew = root.resolve("constantNew");
        ClassPath.compile(constantNew, List.of(), ClassPath.write(root.resolve("source2"), "Constant",
                "public class Constant { public static final int VALUE = 8; }"));
        Path measured = root.resolve("measured");
        Path sMeasured = ClassPath.write(source, "Measured",
                "public class Measured { public static int seen = -1;\n"
              + "  public static void main(String[] a) { seen = Constant.VALUE; } }");
        ClassPath.compile(measured, List.of(constantD), sMeasured);
        int oldValue = ClassPath.readField(List.of(measured, constantNew), "Measured", "seen");
        ClassPath.defect("constant changed, caller not recompiled", oldValue == 8 ? "runtime" : "silent");
        System.out.println("constant on the new class path : 8");
        System.out.println("value seen by the caller        : " + oldValue);

        System.out.println();
        ClassPath.summary();
    }
}
```

```
-- six types, which class path each is required on --
Constant (compile-time constant) compile
Tool (direct dependency)         both
Deep (transitive dependency)     runtime
Facade (interface)               both
ProviderA (implementation)       runtime
ProviderB (second implementation) runtime

-- transitive dependency: Deep never enters the compile class path via Tool --
did User compile against the [Constant, Tool] compile class path: true
Deep is not on this compile class path at all, yet User compiles

-- conflict resolution: two implementations on the runtime path, order decides the choice --
selected when A is given first: A
selected when B is given first: B

-- where the defect appears once the paths diverge --
type absent on the compile path  compile
type absent on the runtime path  runtime
two implementations on the runtime path silent
constant changed, caller not recompiled silent
constant on the new class path : 8
value seen by the caller        : 7

type   : 1 compile only, 3 runtime only, 2 both
defect : 1 compile error, 1 runtime error, 2 silent
```

The first four of the six lines were obtained by compiling and running each type one at a
time; the runtime column of the last two lines is given without a separate measurement. The
first three lines run `User`, the next three run `Caller` source, first trying to compile then
run it against a class path that lacks that type. If compiling fails, the type is required at
compile time; if running fails, it is required at run time. `Constant` turns out required only
at compile time — its value is embedded at the call site, so it is never looked for on the
runtime class path at all. `Tool` and `Facade` are required on both paths: both are named
directly in the source, both have their class files looked for at both the compile and the
runtime stage. `Deep`, `ProviderA`, and `ProviderB` are required only at run time — none of the
three is ever written directly anywhere in the source text.

Result: **of the six types, one is required only on the compile class path, three only on the
runtime class path, two on both.** This number is the shared measurement of this topic and
stays unchanged throughout it.

## Transitive Dependency and Conflict Resolution

`Deep`'s situation is the most misleading of all. `Tool` calls `Deep` directly — but `User`,
when it calls `Tool`, never names `Deep` at all. When the compile class path contains only
`Constant` and `Tool`, `User` compiles without trouble; `Deep` is not on that path at all. This
is direct proof that a **transitive dependency** can be required at run time without ever
appearing on the compile class path — a compile succeeding does not show that the runtime class
path is complete.

The same source text also brings out a separate question: when two implementations of `Facade`
(`ProviderA`, `ProviderB`) are both present on the runtime class path at the same time, which
one is selected? `Caller` scans the service declaration and takes the first one it finds; which
one is found first depends on the **order** they were given to the class path in. When `A` is
given first, `A` is selected; when `B` is given first, `B` is selected — **conflict
resolution** here is not a rule, it is the result of an ordering. The code never throws an error
anywhere; two different setups produce two different answers from the same program.

## Limiting Measurement: The Two Faces of a Silent Defect

- **BT12.** The "silent" defect class gathers cases where neither the compiler nor the runtime
  says anything, but the result comes out different from what was expected; the expected value
  is also written down on every silent line.
- **BT13.** When `Constant` is changed and the version on the runtime class path is changed
  without `User` being recompiled, the difference between the two sides is measured only by
  the value of the field being read; no error is thrown.

The same measurement sorts four separate defects into three classes: the absence of a type on
the compile class path is rejected by the compiler (`compile`), the absence of a type on the
runtime class path throws an error at run time (`runtime`), and the remaining two say nothing at
all (`silent`). Two implementations being found on the runtime path is one of these — the
program produces two different answers without ever throwing an error, and which answer is
"correct" cannot be told from the code itself.

The second silent defect is more subtle: `Constant`'s value is raised to **8**, but `User` is
not recompiled against this new version. Even though the `Constant` on the runtime class path
now carries **8**, the value already embedded in `User`'s class file stays at **7** — because
the embedding happened at compile time, not at run time. The class path looks updated, but the
compiled class still carries the old value; this line falls into the silent class because
neither compiling nor running produces a complaint — the result comes out wrong, nothing more.

In total: **of four defects, one is a compile error, one is a runtime error, two are silent.**
"It compiled" is therefore not proof — a transitive dependency can be entirely absent from the
compile class path while the source compiles without trouble, yet still be required at run
time; a compile succeeding does not show that the runtime class path is complete. A type not
being required on the runtime class path does not mean it is not depended on either — the
absence of `Constant` means it was **copied**, and that copy silently produces a wrong result
once the two sides diverge.

## Summary

- Dependency scope takes three values (compile, runtime, both); measured across six types, the
  distribution comes out **1 compile only, 3 runtime only, 2 both**, and it cannot be extracted
  from the source text.
- A transitive dependency (`Deep`) compiles without trouble while never appearing on the
  compile class path at all, but is required at run time; "it compiled" is not proof.
- Conflict resolution becomes order-dependent once two implementations of the same interface
  are both present on the runtime class path; there is no error, the result changes from setup
  to setup.
- Of four defects, one is a compile error, one is a runtime error, two are silent: in silent
  defects neither the compiler nor the runtime says anything, and the result comes out wrong,
  nothing more.
- A type not being required on the runtime class path does not mean it is not depended on: its
  value may have been embedded, and that embedded value silently goes stale once the source
  changes.

## Next Step

This lesson measured six standalone types — each stayed within the boundary of a single class
or interface. But a real project is made not of single-file classes, it is made of interdependent
**modules**, and the question of what a module's boundary corresponds to at compile time has not
yet been asked. The next lesson measures that: when a type a module considers "internal" is
reached across the boundary, is it caught at compile time, at run time, or not caught at all —
and is the source of that catch really a directory separation.
