skip to content

Services & ServiceLoader (SPI)

ServiceLoader discovers implementations lazily, with uses and provides replacing META-INF/services inside modules. It is the JDK's built-in plugin mechanism and a neat example of decoupling consumer from provider.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

In JPMS, how do the `provides ... with` and `uses` directives work, and how do they replace the META-INF/services file?

level: middleimportance: must knowfreq 60%

answer

  1. provides Service with Impl; on the provider side
  2. uses Service; on the consumer side (mandatory or empty results)
  3. Modular replacement for META-INF/services file
  4. Type-checked at compile/link time, not runtime
  5. Impl package need NOT be exported — provides grants access

basics

~20 s

In a module's module-info.java, the provider writes provides Service with Impl; to declare its implementation, and the consumer writes uses Service; to declare it loads that service. These two lines replace the old META-INF/services text file when using modules.

solid answer

~50 s

With the Java Platform Module System (JPMS), service wiring moves from a text file into the module descriptor (module-info.java). The provider module declares `provides com.example.Service with com.example.ServiceImpl;` — this is the modular equivalent of a META-INF/services entry. The consumer module declares `uses com.example.Service;` — this tells the module system (and ServiceLoader) that this module consumes that service, and it is required for ServiceLoader.load to find providers on the module path. Both directives are checked at compile and link time: the named classes must exist and ServiceImpl must be assignable to Service, so misconfiguration fails early rather than at runtime. Crucially, the provider does NOT need to `exports` the package containing ServiceImpl — the module system can instantiate it through the provides directive even though it stays encapsulated. You can also list multiple implementations: `provides Service with ImplA, ImplB;`.

code

java · 22 lines
java
// === Service module: com.example.imageapi ===
module com.example.imageapi {
    exports com.example.imageapi;            // the interface IS exported
}

// === Provider module: com.example.png ===
module com.example.png {
    requires com.example.imageapi;
    // NOTE: package com.example.png is NOT exported, yet provides still works
    provides com.example.imageapi.ImageReader
        with com.example.png.PngReader;
}

// === Consumer module: com.example.app ===
module com.example.app {
    requires com.example.imageapi;
    uses com.example.imageapi.ImageReader;   // mandatory for ServiceLoader on the module path
}

// In the consumer's code:
ServiceLoader<ImageReader> readers = ServiceLoader.load(ImageReader.class);
readers.forEach(r -> System.out.println(r.getClass()));

go deeper

for a junior

Recognizes the two keywords: provides...with on the provider, uses on the consumer, and that together they wire a service in modules.

for a middle

Writes correct module-info directives, knows uses is mandatory for discovery, and that they replace META-INF/services for modules.

for a senior

Explains compile/link-time type checking, that the impl package stays unexported, and how classpath vs module-path discovery coexist.

for a principal

Reasons about migration strategy (dual classpath+module support), encapsulation/security implications of provides-without-exports, and tooling/jlink integration.

## Background: what JPMS is The **Java Platform Module System (JPMS)**, introduced in Java 9, groups packages into a **module** described by a file named `module-info.java` (the *module descriptor*). A module explicitly states what it **`requires`** (depends on), what packages it **`exports`** (makes visible to others), and — relevant here — what services it **`provides`** and **`uses`**. Strong encapsulation is the headline feature: a package is invisible to other modules unless explicitly exported. ## The old way: META-INF/services Before modules, a provider jar declared its implementation in a **text file** at `META-INF/services/<fully-qualified-service-name>`, each line naming an implementing class. ServiceLoader read these files off the classpath. Problems: it is **stringly typed** (a typo or stale class name only blows up at runtime via `ServiceConfigurationError`), the build has **no idea** the relationship exists, and it cannot work with strong encapsulation — the implementing class would need to be reflectively reachable. ## The module way: two directives JPMS turns both halves of the relationship into **first-class declarations** inside `module-info.java`. ### Provider side — `provides ... with` ```java module com.example.png { requires com.example.imageapi; // where Service lives provides com.example.imageapi.ImageReader // the SERVICE TYPE with com.example.png.PngReader; // the IMPLEMENTATION class } ``` Read it as: *this module provides an implementation of `ImageReader`, namely `PngReader`.* You can supply several: `with PngReader, PngReaderV2;`. ### Consumer side — `uses` ```java module com.example.app { requires com.example.imageapi; uses com.example.imageapi.ImageReader; // I will ServiceLoader.load this } ``` Read it as: *this module consumes the `ImageReader` service.* **Without the `uses` directive, `ServiceLoader.load(ImageReader.class)` on the module path returns nothing** — the module system only surfaces providers for services a module has declared it uses. ## How this *replaces* the file The `provides ... with` directive is the **exact modular substitute** for a `META-INF/services/<service>` entry. Conceptually the compiler/runtime synthesizes the same "this class implements this service" fact, but now: - **It is type-checked.** At compile (and `jlink` link) time, the compiler verifies the service type and the impl class both exist and that the impl is **assignable to** (a subtype of) the service. A typo is a *compile error*, not a runtime `ServiceConfigurationError`. - **The build understands it.** Tools see the dependency in the descriptor. - **It respects encapsulation.** This is the subtle, important part: the provider module **does not have to `exports` the package** that contains `PngReader`. Normally another module could never instantiate an unexported class — but the `provides` directive grants the module system *just enough* privileged access to construct that one class, so the implementation stays hidden from everyone else. With the old file mechanism, the impl effectively had to be openly reachable. ## Precedence and coexistence On the **classpath**, the old `META-INF/services` file still works (and is what non-modular jars use). On the **module path**, the descriptor directives drive discovery. A modular jar can contain *both* for compatibility (so it works as an automatic/classpath jar too); when run as a real module, the descriptor wins. ## Common pitfalls - **Forgetting `uses` in the consumer** → empty ServiceLoader results on the module path (the #1 gotcha). - Thinking you must `exports` the impl package — you must **not**; that leaks it. `provides` alone suffices. - Assuming the directive picks an instance — it only declares availability; `ServiceLoader` still does the lazy instantiation and ordering is unspecified.

  • If you omit `uses` but keep `provides`, what happens when the consumer calls ServiceLoader.load on the module path?
    It returns no providers. On the module path, ServiceLoader only surfaces providers for services the consuming module has declared with `uses`. Adding `uses Service;` fixes it.
  • Must the provider export the package holding the implementation class?
    No, and it should not. The `provides ... with` directive lets the module system instantiate that class while it stays unexported (encapsulated). Exporting it would needlessly leak the implementation.

saying these in an interview costs you the question

  • Forgetting that the consumer needs `uses`, leading to silently empty ServiceLoader results on the module path
  • Claiming you must `exports` the implementation package (you must not — provides handles access)
  • Saying the directives instantiate or pick the provider (they only declare; ServiceLoader still loads lazily)
  • Thinking provides...with works on the classpath (on the classpath the META-INF/services file is what's used)

context

open as a page

What is a Service Provider Interface (SPI) in Java, and how does ServiceLoader let code discover implementations at runtime?

level: juniorimportance: should knowfreq 55%

basics

~10 s

An SPI is an interface (or abstract class) that other code provides implementations for. ServiceLoader.load(MyService.class) finds and loads those implementations at runtime, so your program can use plug-ins it wasn't compiled against.

open as a page

What is ServiceConfigurationError, when is it thrown, and what are the common operational pitfalls when wiring SPI across the classpath and module path?

level: seniorimportance: should knowfreq 35%

basics

~20 s

ServiceConfigurationError is thrown when a declared provider can't be loaded or instantiated — bad class name, no public no-arg constructor, or it doesn't implement the service. It surfaces while you iterate the ServiceLoader, not when you call load(). Typical pitfalls: forgetting uses on the module path, stale META-INF/services entries, and missing constructors.

open as a page

How does ServiceLoader instantiate providers lazily, and how does the stream()/Provider API let you inspect a provider before creating it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

ServiceLoader only creates a provider object when you actually reach it while iterating, not up front. Since Java 9, ServiceLoader.stream() gives a stream of Provider handles, each exposing type() so you can check the class and call get() to instantiate only the one(s) you want.

open as a page

When should you reach for ServiceLoader/SPI versus a dependency-injection framework or a hand-rolled plugin registry? What are the design tradeoffs?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Use ServiceLoader/SPI when you want zero-dependency, JDK-native plug-in discovery — third parties extend your library by dropping in a jar/module. Use a DI framework when you need rich lifecycle, scoping, and wiring of many beans inside one app. SPI is for open extension points; DI is for assembling your own application's objects.

open as a page