skip to content

A class that ships inside the JDK — say a factory that discovers implementations through java.util.ServiceLoader — needs to instantiate an implementation class that lives in your application's JAR. Strict parent-first delegation makes that impossible, because the JDK's loader cannot see application classes. How does the platform get around this?

level: seniorimportance: should knowfreq 45%

answer

  1. Delegation is one-way: parents can't see children
  2. SPI: interface high, implementation low
  3. Thread.currentThread().getContextClassLoader()
  4. ServiceLoader.load(S.class) uses TCCL; 2-arg overload is explicit
  5. Pool threads carry a stale TCCL — set and restore in finally

basics

~20 s

By deliberately inverting delegation: the JDK code obtains a loader that can see application classes and loads through it. Usually that is the thread context class loader, Thread.currentThread().getContextClassLoader(), which defaults to the application loader; APIs like ServiceLoader also accept an explicit loader argument.

solid answer

~60 s

Parent-first delegation is directional: a child can see everything its ancestors can, but a parent can never see its children's classes. Service-provider interfaces invert that requirement — the *interface* lives high (in the JDK), the *implementation* lives low (in the application). The standard escape hatch is the **thread context class loader**. Every thread carries a `ClassLoader` reference, settable via `Thread.setContextClassLoader` and readable via `getContextClassLoader`; the main thread's is initialised to the system (application) loader and threads inherit it from their creator. JDK code that must load an application-supplied implementation asks the thread for that loader instead of using its own, which effectively lets it reach downward. `ServiceLoader.load(Service.class)` uses the context loader by default; the two-argument `ServiceLoader.load(Service.class, loader)` overload takes the loader explicitly, which is the more predictable form. Some older APIs instead use the caller's loader. The operational catch: in containers and thread pools the context loader may be wrong — inherited from whichever thread happened to create the pool. The discipline is to set it around the call and restore it in a `finally`.

code

java · 12 lines
java
// Preferred: say which loader should supply implementations
ServiceLoader<Codec> codecs = ServiceLoader.load(Codec.class, pluginLoader);

// If an API insists on the thread context loader, scope the change
Thread t = Thread.currentThread();
ClassLoader saved = t.getContextClassLoader();
try {
    t.setContextClassLoader(pluginLoader);
    ServiceLoader.load(Codec.class).forEach(this::register); // uses TCCL
} finally {
    t.setContextClassLoader(saved);
}

go deeper

for a junior

Know that delegation only goes upward and that the thread context class loader is the standard way JDK code reaches application-provided implementations.

for a middle

Explain the SPI shape — interface high, implementation low — and that ServiceLoader.load uses the context loader unless you pass one explicitly.

for a senior

Bring the failure modes: stale context loaders on pool threads, container set-and-restore behaviour, and why passing the loader explicitly is the safer API design.

for a principal

Treat it as a boundary contract: decide per component which loader resolves configured class names, prefer explicit loader parameters over ambient state, and account for the lifecycle implications of holding a component loader.

## The directional problem Delegation makes visibility strictly one-way. A loader can see every class its ancestors can define, because it asks them. An ancestor can see nothing its descendants define, because it never asks downward — asking downward would immediately destroy the single-definition guarantee that makes delegation worth having. That is fine until you hit the service-provider pattern, which is deliberately shaped the other way: - The **interface or abstract API** is a shared contract, so it must live high (JDK or container) to keep exactly one definition. - The **implementation** is pluggable and application-specific, so it lives low (application class path or a plugin loader). - And the **code that must instantiate the implementation** is the API's own factory — living high. JDBC, JAXP, JNDI, JCE providers, logging bindings and every `ServiceLoader`-based mechanism have this shape. The high code must construct a low class. Strict parent-first cannot do it. ## The thread context class loader The platform's answer is to attach a loader to the *thread* rather than to the class doing the loading. `java.lang.Thread` carries a context class loader: ```java ClassLoader cl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(cl); ``` Properties worth knowing: - The primordial thread's context loader is initialised to the **system (application) class loader**. - A new thread **inherits** its creator's context loader at construction time. - Any code can set it, so it is a mutable, ambient, thread-local piece of state. JDK code that needs an application-supplied class reads this loader and loads through it. Because that loader is a descendant of the JDK's own, it can see both the shared interface (via delegation upward) and the implementation (locally). Delegation has been inverted *for that specific lookup* while remaining intact everywhere else. ## ServiceLoader specifics `ServiceLoader.load(Service.class)` is documented to use the current thread's context class loader. That is convenient and also the source of most of the surprises, because it depends on ambient state you may not control. `ServiceLoader.load(Service.class, someLoader)` takes the loader explicitly and is preferable whenever you know which loader should provide implementations — typically `Service.class.getClassLoader()` in a plugin-free application, or the plugin's loader in a container. Library authors should expose a loader parameter rather than silently depending on the context loader. In a modular application, `ServiceLoader` also resolves providers declared with `provides ... with ...` in module descriptors, and `ServiceLoader.load(ModuleLayer, Service.class)` searches a specific layer — a cleaner, declarative alternative to the ambient loader for modular deployments. ## Related inversions - **JDBC.** Historically `Class.forName` on a driver name registered it with `DriverManager`; modern drivers are discovered via `ServiceLoader`. `DriverManager` additionally screens candidate drivers by whether the *caller's* loader can see them, which is another form of reaching downward safely. - **Logging facades and JAXP factories** read system properties and then load the named class through the context loader. - **Frameworks generally** — anything that instantiates a class named in configuration must decide *which loader* resolves that name, and the context loader is the usual default. ## Where it goes wrong operationally The context loader is ambient state, so it is only correct if someone set it correctly: - **Thread pools.** A pool thread keeps the context loader of whichever thread created it, forever. If a container created the pool during startup, tasks submitted later from a plugin still run with the container's loader and cannot see plugin classes. Symptom: `ClassNotFoundException` or `ServiceConfigurationError` for a class that is demonstrably on the plugin's class path, and which works when invoked on the request thread. - **Containers.** Servlet containers set the context loader to the web application's loader before invoking application code and restore it afterwards. Anything running outside that window — a background timer, an asynchronous callback — does not get that treatment. - **Leaks.** Holding a reference to a web application's loader in a long-lived thread's context field keeps the entire application's classes reachable after undeploy. ## The discipline ```java Thread t = Thread.currentThread(); ClassLoader saved = t.getContextClassLoader(); try { t.setContextClassLoader(pluginLoader); ServiceLoader.load(Codec.class).forEach(this::register); } finally { t.setContextClassLoader(saved); // always restore } ``` Set it narrowly around the call, always restore in `finally`, and prefer passing the loader explicitly when the API allows. Treat a context-loader dependency in a library as something to document loudly, because it makes the library's behaviour depend on invisible state. ## Interview framing "Delegation is one-way, so a parent-loaded factory can't see a child-loaded implementation. The platform inverts it deliberately through the thread context class loader — `ServiceLoader.load(Service.class)` uses it by default, and the two-arg overload lets you pass a loader explicitly, which I prefer. The failure mode to watch is a pooled thread carrying a stale context loader, which produces ClassNotFoundException for classes that are obviously present."

  • A plugin's ServiceLoader lookup works on request threads but throws ClassNotFoundException when the same code runs on a thread-pool thread. What is happening?
    The pool's threads inherited their context class loader from whichever thread created the pool — typically a container startup thread whose loader cannot see plugin classes. Since ServiceLoader.load(Service.class) resolves providers through the context loader, the lookup searches the wrong loader and finds nothing, even though the provider is plainly on the plugin's class path. Fix it by passing the plugin loader explicitly to ServiceLoader.load, or by setting and restoring the context loader inside the submitted task.
  • Why is depending on the thread context class loader considered fragile in library code?
    Because it is ambient, mutable, thread-local state that the library neither owns nor can validate. The same call behaves differently depending on which thread it runs on and who last set the field, which makes failures environment-dependent and hard to reproduce. It also encourages loader leaks, since a long-lived thread holding a component's loader keeps that component's classes reachable after it should have been discarded. Accepting an explicit ClassLoader parameter removes all of that.

A head office writes the standard but the branch offices supply the staff. Head office cannot see branch payroll, so each visitor carries a badge naming their branch — the thread context loader is that badge, and head office reads it to find the right people.

saying these in an interview costs you the question

  • Claiming a parent loader can simply ask its children for a class — delegation has no downward path
  • Thinking the context class loader is derived from the class doing the loading rather than from the current thread
  • Assuming a thread's context loader is always the application loader, ignoring inheritance in thread pools
  • Using ServiceLoader.load(Service.class) inside a container without knowing which loader it will consult
  • Setting the context class loader and never restoring it, leaking a component's loader into a long-lived thread

context