🪞 Reflection, Annotations & Modules · Advanced

ServiceLoader in Java

Plug-in discovery with provides/uses.

🧩 The mysteryYou drop a new payment JAR into a folder and the app starts using it, with no code change and no recompile. How did the app find a class it never mentions?

A job board for implementations

**ServiceLoader separates an API from its implementations. The consumer depends only on an interface and declares uses. Each provider declares provides ... with ...**. At runtime, ServiceLoader.load(Gateway.class) discovers them.

module com.shop.app {        // consumer
    uses com.pay.Gateway;
}
module com.stripe.pay {      // provider
    provides com.pay.Gateway
        with com.stripe.StripeGateway;
}

On the class path

Without modules, a provider registers itself with a text file: **META-INF/services/com.pay.Gateway**, containing the implementation's class name. Consumer code is the same either way.

// META-INF/services/com.pay.Gateway
com.stripe.StripeGateway
 
var loader = ServiceLoader.load(Gateway.class);
for (Gateway g : loader) g.connect();
🔮 Predict it

Nobody applied

What does this print?

interface Codec { String name(); }
void main() {
    var all = ServiceLoader.load(Codec.class);
    var first = all.findFirst();
    System.out.println(all.stream().count());
    System.out.println(first.isPresent());
}
  1. 0 false
  2. 1 true
  3. Throws ServiceConfigurationError
Show the answer

ServiceLoader only finds registered providers. Nothing provides Codec here, so the stream is empty and findFirst() returns an empty Optional. No error, just nothing.

Provider rules

ServiceLoader must create providers knowing nothing else about them. So a provider needs either a public no-arg constructor or, in a named module, a **public static provider() method** that returns the instance.

⚠️ The trap

Writing the class isn't enough

Implementing the interface doesn't register anything. Forget the provides line (or the META-INF/services file) and ServiceLoader silently finds nothing. Also mind the words: the provider says **provides, the consumer says uses**, and exports registers nothing.

module com.stripe.pay {
    requires com.pay.api;
    // missing: provides ... with ...
}
💼 In the real world

Plug-ins in the JDK itself

Logging facades find their backend this way: SLF4J 2 discovers a binding like Logback through ServiceLoader. The app only knows the interface, so new providers work just by being added to the module path or class path.

Key takeaways

  1. Consumer: uses + ServiceLoader.load
  2. Provider: provides ... with ...
  3. Class path: META-INF/services/<interface name>
  4. Providers need a public no-arg constructor or provider() method

💡 ServiceLoader is a job board: the app posts the role (interface), and any provider JAR that applies gets hired at runtime.

🤯 Did you know?

Since JDBC 4.0 (Java 6), database drivers register through META-INF/services/java.sql.Driver, which is why modern code never needs Class.forName("com.mysql.Driver").

Practice questions

What does this print?

interface Plugin { String name(); }
void main() {
    var found = ServiceLoader.load(Plugin.class)
            .findFirst();
    System.out.println(found.isPresent());
}
  1. false
  2. Throws ServiceConfigurationError
  3. true
Check your answer

false. ServiceLoader only finds registered providers. Nothing provides Plugin here, so findFirst() returns an empty Optional.

New payment gateways should work just by dropping a JAR on the module path, without recompiling the app. Which design fits?

  1. A Gateway interface discovered with ServiceLoader (uses/provides)
  2. A static Map filled in main()
  3. Class.forName over a hard-coded list of names
  4. A switch statement over gateway names
Check your answer

A Gateway interface discovered with ServiceLoader (uses/provides). ServiceLoader is the JDK's built-in plug-in mechanism: the app only knows the interface, and providers register themselves.

Modules also changed something tiny but delightful: imports. Next: one line that imports a whole module.