---
title: 'Logging Facade and Implementation'
source: 'https://academia.sh/en/courses/java-ecosystem/logging-facade-and-implementation'
course: 'Build, Testing and the Ecosystem'
language: en
updated: '2026-09-13T05:08:58+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Shows, by reading the constant pool, that the caller's class file references the facade but not the implementation classes"
    - 'Measures that the implementation enters the runtime class path through a service declaration, and that when two implementations are present, the class path order decides the selection'
    - "Shows that when no implementation is found at all, the caller compiles and runs but produces no records, and that this silence is not a defect but the mechanism's own contract"
---

# Logging Facade and Implementation

The measurement reads the caller's class file's constant pool to show that it references the facade but never the implementation; the implementation enters the runtime class path through a service declaration, and when two implementations are present, the class path order decides which one is selected. The limiting measurement shows that when no implementation is found at all, the caller still compiles and runs, but produces no records.

We built the class path the test saw and knew exactly what to put on it: which fake type
stood in for which real type, which setup added which dependency to the test class path, all
written step by step by us. In this lesson, the writing side changes. The caller compiles
against an interface and never writes the name of the class that will stand behind it — it
does not import it, does not instantiate it, does not even mention it as a string. Yet when
the program runs, that nameless class is found and called. The question is: how does a name
that was never written in the source enter the runtime class path, and when more than one
candidate is present there, what decides which one wins?

This topic covers logging across three lessons, but this first lesson is not concerned with
keeping a log itself — it is concerned with the dependency direction that a logging facade
sets up. A facade — here, a single-method interface — becomes the only type the caller
speaks to; the implementation behind it never becomes visible to the caller. The measurement
below builds this in four steps: first, that compilation passes without ever seeing the
implementation and that the class file does not reference it; then, by what kind of
declaration the implementation enters the runtime class path; then, when more than one
implementation is present, what decides the winner; and finally, what happens when no
implementation is present at all.

## Only the Facade Is on the Compile Class Path

**LO1.** The caller's source text writes only the name of the facade interface; the names of
the implementation classes never appear in the source, so the compiler has no need to see
them. **LO2.** The class file's constant pool carries only the types whose names appear in
the source; a name never written in the source is absent from the constant pool as well.

The rest of the measurement repeats four tasks: writing a source file to disk, compiling it
against a given class path, running a compiled class with a given class path, and reading
which types are named in a class file's constant pool. These four are gathered in the helper
class below; the topic's remaining two lessons use the same four.

```java
// Tool.java — shared routines for writing sources, compiling, running and reading class files (helper class, no main)
import java.io.*;
import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Tool {
    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, List<Path> sources) throws IOException {
        Files.createDirectories(output);
        List<String> args = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            args.add("-classpath");
            args.add(sj.toString());
        }
        for (Path s : sources) args.add(s.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silentOutput = new StringWriter();
        int result = compiler.run(new PrintWriter(silentOutput), new PrintWriter(silentOutput),
                args.toArray(new String[0]));
        return result == 0;
    }

    static void 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, Tool.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
        }
    }

    static Object field(List<Path> classPath, String mainClass, String fieldName) 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, Tool.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return cls.getField(fieldName).get(null);
        }
    }

    static Set<String> references(Path classFile) throws IOException {
        Set<String> names = new TreeSet<>();
        ClassModel model = ClassFile.of().parse(classFile);
        for (PoolEntry e : model.constantPool()) {
            if (e instanceof ClassEntry ce) names.add(ce.asInternalName().replace('/', '.'));
        }
        return names;
    }

    static void show(String label, Object value) {
        System.out.printf("%-62s: %s%n", label, value);
    }

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

    static String facadeSource() {
        return "public interface LoggingFacade { void write(String message); int recordCount(); }";
    }

    static String callerSource() {
        return "import java.util.ServiceLoader;\n"
             + "public class Caller {\n"
             + "  static final class Silent implements LoggingFacade {\n"
             + "    public void write(String message) { }\n"
             + "    public int recordCount() { return 0; }\n"
             + "  }\n"
             + "  public static String selectedClass = \"none\";\n"
             + "  public static int recordsProduced = -1;\n"
             + "  static LoggingFacade find() {\n"
             + "    for (LoggingFacade g : ServiceLoader.load(LoggingFacade.class, Caller.class.getClassLoader())) return g;\n"
             + "    return new Silent();\n"
             + "  }\n"
             + "  public static void main (String[] a) {\n"
             + "    LoggingFacade g = find();\n"
             + "    selectedClass = g.getClass().getSimpleName();\n"
             + "    g.write(\"start\"); g.write(\"progress\"); g.write(\"end\");\n"
             + "    recordsProduced = g.recordCount();\n"
             + "  }\n"
             + "}";
    }

    static String implementationSource(String name) {
        return "public class " + name + " implements LoggingFacade {\n"
             + "  private int counter = 0;\n"
             + "  public void write(String message) { counter++; }\n"
             + "  public int recordCount() { return counter; }\n"
             + "}";
    }
}
```

`callerSource()` produces the caller's source: `Caller` imports only the `LoggingFacade`
interface and `ServiceLoader`, and writes the name of no implementation class. If it finds
none, it falls back to the `Silent` inner class it defines itself — this, too, is an
implementation, but it lives in the caller's own source, not brought in from outside.

```java
// OnlyFacade.java — does the caller compile when only the facade is on the compile class path, and which types does the class file reference
import java.nio.file.*;
import java.util.*;

public class OnlyFacade {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl1");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());
        Path sImplA = Tool.write(source, "ImplementationA", Tool.implementationSource("ImplementationA"));
        Path sImplB = Tool.write(source, "ImplementationB", Tool.implementationSource("ImplementationB"));

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Path dirA = root.resolve("recordA");
        Path dirB = root.resolve("recordB");

        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(dirA, List.of(facadeDir), List.of(sImplA));
        Tool.compile(dirB, List.of(facadeDir), List.of(sImplB));
        boolean callerCompiled = Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));

        Set<String> refs = Tool.references(callerDir.resolve("Caller.class"));

        Tool.show("did the caller compile with no implementation on the compile class path", callerCompiled);
        Tool.show("does the caller's class file reference LoggingFacade", refs.contains("LoggingFacade"));
        Tool.show("does the caller's class file reference ImplementationA", refs.contains("ImplementationA"));
        Tool.show("does the caller's class file reference ImplementationB", refs.contains("ImplementationB"));
    }
}
```

```
did the caller compile with no implementation on the compile class path: true
does the caller's class file reference LoggingFacade          : true
does the caller's class file reference ImplementationA        : false
does the caller's class file reference ImplementationB        : false
```

`ImplementationA` and `ImplementationB` really exist in this measurement: both are compiled,
and both class files sit on disk. Even `callerDir`'s compile class path does not include
them — only `facadeDir` is there — and `Caller` still compiles (LO1). When the constant pool
is read, `LoggingFacade` is there, but `ImplementationA` and `ImplementationB` are not (LO2).
This is not a guess: a name never written in the source appears in none of the instructions
the compiler could produce, because a class file is a translation of the source and invents
nothing the source did not know. This is exactly where a facade's whole power lies: the
caller does not even need to know, at compile time, whether the class that will stand behind
it exists at all.

## The Implementation Enters the Path via a Declaration

**LO3.** The implementation enters the runtime class path through a service declaration
file: a file named `META-INF/services/<facade-interface's-full-name>` carries the provider
class's name as a line of text. **LO4.** `ServiceLoader.load(...)` reads this file to find
and instantiate the provider; the caller never calls the provider's constructor anywhere in
its own code.

```java
// ServiceDeclaration.java — the implementation enters the path through a service declaration, the caller finds it with ServiceLoader
import java.nio.file.*;
import java.util.*;

public class ServiceDeclaration {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl2");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());
        Path sImplA = Tool.write(source, "ImplementationA", Tool.implementationSource("ImplementationA"));

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Path dirA = root.resolve("recordA");

        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));
        Tool.compile(dirA, List.of(facadeDir), List.of(sImplA));
        Tool.declare(dirA, "LoggingFacade", "ImplementationA");

        Object selectedBefore = Tool.field(List.of(callerDir, facadeDir), "Caller", "selectedClass");
        Tool.show("class the caller falls back to when no implementation is on the runtime path", selectedBefore);

        List<Path> runtimePathAfter = List.of(callerDir, facadeDir, dirA);
        Object selectedAfter = Tool.field(runtimePathAfter, "Caller", "selectedClass");
        Object recordsAfter = Tool.field(runtimePathAfter, "Caller", "recordsProduced");
        Tool.show("class selected once the implementation enters the path through a declaration", selectedAfter);
        Tool.show("number of records produced", recordsAfter);
    }
}
```

```
class the caller falls back to when no implementation is on the runtime path: Silent
class selected once the implementation enters the path through a declaration: ImplementationA
number of records produced                                    : 3
```

The `dirA` directory carries two things: `ImplementationA.class` and a declaration file
named `META-INF/services/LoggingFacade` that holds the single line `ImplementationA`. Once
`dirA` is added to the runtime class path, the `ServiceLoader.load` call inside `Caller`
reads this file, loads `ImplementationA`, and instantiates it — without a single line of code
ever writing `new ImplementationA()` (LO3, LO4). All three `write` calls reach this instance,
and the counter climbs to three. Once `dirA` is removed from the runtime class path, the same
`Caller.class`, without any recompilation, falls back to its own `Silent` again. The only
thing that changes is the content of the runtime class path; the `Caller.class` file is the
exact same byte sequence in both cases.

## With Two Implementations, the Path Order Decides

**LO5.** When more than one provider is declared for the same service on the runtime class
path, the first provider `ServiceLoader` returns depends on the order of the class path
entries. **LO6.** When the order changes, the selected implementation changes, but neither
compilation nor execution fails — both end with an "OK" result.

```java
// OrderWinner.java — with two implementations on the runtime class path, the path order decides the winner
import java.nio.file.*;
import java.util.*;

public class OrderWinner {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl3");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());
        Path sImplA = Tool.write(source, "ImplementationA", Tool.implementationSource("ImplementationA"));
        Path sImplB = Tool.write(source, "ImplementationB", Tool.implementationSource("ImplementationB"));

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Path dirA = root.resolve("recordA");
        Path dirB = root.resolve("recordB");

        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));
        Tool.compile(dirA, List.of(facadeDir), List.of(sImplA));
        Tool.compile(dirB, List.of(facadeDir), List.of(sImplB));
        Tool.declare(dirA, "LoggingFacade", "ImplementationA");
        Tool.declare(dirB, "LoggingFacade", "ImplementationB");

        Object aFirst = Tool.field(List.of(callerDir, facadeDir, dirA, dirB), "Caller", "selectedClass");
        Object bFirst = Tool.field(List.of(callerDir, facadeDir, dirB, dirA), "Caller", "selectedClass");

        Tool.show("selected class when A is given first on the runtime class path", aFirst);
        Tool.show("selected class when B is given first on the runtime class path", bFirst);
        Tool.show("did both run without error and differ only by order", !aFirst.equals(bFirst));
    }
}
```

```
selected class when A is given first on the runtime class path: ImplementationA
selected class when B is given first on the runtime class path: ImplementationB
did both run without error and differ only by order           : true
```

`dirA` and `dirB` are two directories unaware of each other; each carries its own class and
its own service declaration. When the runtime class path is set up in the order
`[callerDir, facadeDir, dirA, dirB]`, `ServiceLoader` reaches `dirA`'s declaration first and
returns `ImplementationA`; when the same two directories are given in the order `dirB, dirA`,
the winner becomes `ImplementationB` (LO5). `Caller`'s source, the `LoggingFacade` interface,
and the two implementations themselves — none of these changed between the two runs; only the
order of the list changed. Nowhere in the code is there a conflict warning or an error (LO6):
finding two providers falls into the same pattern as K05's shared definition — "when two
implementations are found on the runtime class path, the selection changes with the path
order, with no error" — seen once more here at the interface level.

## When No Implementation Is Found

**LO7.** When no provider is found on the runtime class path, `ServiceLoader.load(...)`
returns an empty iterator; the caller meets this not with an exception, but with a default it
defines itself. **LO8.** In this case the program runs without error, but because no
implementation increments `recordCount()`, the number of records produced is zero.

```java
// NoImplementation.java — with no implementation on the runtime path at all, the caller runs without error but produces no records
import java.nio.file.*;
import java.util.*;

public class NoImplementation {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl4");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));

        List<Path> runtimePath = List.of(callerDir, facadeDir);
        boolean ranWithoutError;
        try {
            Tool.run(runtimePath, "Caller");
            ranWithoutError = true;
        } catch (Exception e) {
            ranWithoutError = false;
        }
        Object selected = Tool.field(runtimePath, "Caller", "selectedClass");
        Object records = Tool.field(runtimePath, "Caller", "recordsProduced");

        Tool.show("does it run without error when no implementation is on the runtime path", ranWithoutError);
        Tool.show("class fallen back to", selected);
        Tool.show("number of records produced", records);
    }
}
```

```
does it run without error when no implementation is on the runtime path: true
class fallen back to                                          : Silent
number of records produced                                    : 0
```

This measurement's runtime class path has neither `ImplementationA` nor `ImplementationB` —
only the facade and the caller are there. The program still completes, throwing no exception
(LO7), and `recordsProduced` stays at zero (LO8). This is the lesson's limiting measurement:
the facade does not remove the dependency between caller and implementation, it reverses its
direction. When no implementation exists at all, the caller sees this not as an error but as
a gap, and fills it with its own `Silent` fallback. The number of records produced being zero
is not the sign of a defect here; it is the outcome of a rule the caller wrote in its own
source. A logging facade's real-world implementations carry the same promise: when no
implementation is wired in, the program does not crash, it just stays silent. This is the
real guarantee that the facade mechanism gives the caller — the caller itself can be written
without knowing which implementation will enter the runtime class path, because the worst
case is "no records at all," not "the program does not run." This guarantee carries a cost
as well: when the runtime class path is set up incorrectly, there is no error message
anywhere, so noticing the records that are missing is far harder than noticing a line the
compiler rejected.

## Summary

- The caller's source text writes only the name of the facade interface; the caller compiles
  even when no implementation at all is present on the compile class path.
- The class file's constant pool carries the types written in the source: `LoggingFacade` is
  there, but `ImplementationA` and `ImplementationB`, which never appear in the source, are
  not.
- The implementation enters the runtime class path through a service declaration file;
  `ServiceLoader` reads this declaration to find and instantiate the provider without its
  name ever appearing in a single line of the caller.
- When more than one provider is declared on the runtime class path, the order of the class
  path entries decides the winner; the selection changes when the order changes, but no
  error occurs.
- Limiting measurement: when no implementation is found at all, the caller still compiles and
  runs without error, but produces no records — the facade does not remove the dependency, it
  reverses its direction.

## Next Step

What this lesson measured was how a name enters the path: through a declaration, through
order, or not at all. The next lesson holds the same set of calls fixed and turns the one
thing that changes into a threshold — when the same three calls are run against two different
thresholds, how many of them actually turn into a record, and does a call that falls below
the threshold still carry a cost?
