skip to content

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

level: juniorimportance: should knowfreq 55%

answer

  1. Interface = service; class = provider; ServiceLoader = finder
  2. Consumer depends on interface only, providers found at runtime
  3. ServiceLoader.load(X.class) returns an Iterable
  4. Two declaration mechanisms: META-INF/services file vs provides...with
  5. JDBC drivers / charset providers use it

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.

solid answer

~40 s

A Service Provider Interface (SPI) is a contract — usually a Java interface or abstract class — defined by a consumer that wants pluggable behaviour. Other parties (providers) supply concrete implementations. At runtime the consumer calls ServiceLoader.load(MyService.class), which returns an Iterable of available implementations discovered from the classpath or module path. The consumer never references a concrete class by name; it depends only on the interface. This decouples the consumer from providers, so you can add or swap implementations by dropping a new jar/module in, with no recompilation. The JDK itself uses this pattern heavily — for example JDBC drivers (java.sql.Driver), charset providers, and logging backends are all loaded this way. ServiceLoader instantiates each provider lazily as you iterate.

go deeper

for a junior

Can state that an SPI is an interface, providers implement it, and ServiceLoader.load finds them at runtime so you can use plug-ins.

for a middle

Explains the two declaration mechanisms (META-INF/services vs provides...with), the no-arg constructor / provider() requirement, and lazy iteration.

for a senior

Articulates the decoupling/extensibility rationale, lazy instantiation, caching/reload, ServiceConfigurationError timing, and concrete JDK uses.

for a principal

Frames SPI as a plug-in architecture pattern, weighs it against DI frameworks, discusses module-path encapsulation tradeoffs and versioning/conflict strategy across providers.

## The problem SPI solves Imagine you write a program that needs to read image files, but you want others to add support for new formats (PNG, GIF, your-own-format) **without you recompiling your program**. You need a way to say "any class out there that implements my `ImageReader` interface, find it and use it." That mechanism is the **Service Provider Interface (SPI)** plus the `java.util.ServiceLoader` class. ## Vocabulary (defined from scratch) - **Service**: a feature or capability, expressed as a Java **interface** or **abstract class**. Example: `interface ImageReader { Image read(File f); }`. This is also called the **service type** or the **SPI**. - **Service provider** (or just *provider*): a concrete **class that implements** the service interface. Example: `class PngReader implements ImageReader`. - **Consumer / client**: the code that *uses* the service. It knows only the interface, never the concrete provider class. - **ServiceLoader**: the JDK class (`java.util.ServiceLoader`) that, given a service type, **discovers** all providers and gives them to you one at a time. The whole point: the consumer depends on the **interface only**. Providers are found at **runtime**, not chosen at compile time. This is **inversion of control** for class selection. ## How discovery works (two mechanisms) ServiceLoader must learn *which* classes implement a given interface. It does this through declared metadata, not by scanning every class (which would be slow): 1. **Classpath / pre-module mechanism — the `META-INF/services` file.** A provider jar contains a plain text file at `META-INF/services/<fully.qualified.ServiceInterfaceName>`. Each line is the fully-qualified name of one implementing class. Example: a file named `META-INF/services/com.example.ImageReader` containing the line `com.example.png.PngReader`. 2. **Module path / JPMS mechanism — `provides ... with` in `module-info.java`.** In a modular application the provider's module declares `provides com.example.ImageReader with com.example.png.PngReader;` and the consumer's module declares `uses com.example.ImageReader;`. (Covered in depth in the other questions of this leaf.) Both declare the same fact — "this class implements that interface" — just in different places. ## How you call it (the consumer side) ```java ServiceLoader<ImageReader> loader = ServiceLoader.load(ImageReader.class); for (ImageReader reader : loader) { // use each discovered provider } ``` `load` returns a `ServiceLoader<ImageReader>`, which is **`Iterable`**. Iterating it yields each discovered provider, **instantiated lazily** (only when you reach it in the loop) by calling its **public no-argument constructor** (the classic requirement) or its **public static `provider()` method** (a newer alternative — the *provider factory* pattern). ## Where the JDK itself uses this - **JDBC**: `java.sql.Driver` providers are discovered, which is why modern drivers no longer need `Class.forName("...")`. - **`java.nio.charset.spi.CharsetProvider`**, `java.time.chrono.Chronology`, image I/O readers, `java.util.spi.*` locale services, logging backends, etc. ## Why it is valuable - **Decoupling**: consumer ↔ provider share only the interface; you swap implementations by changing the path, not the code. - **Extensibility / plug-ins**: third parties extend your app without touching its source. - **Lazy loading**: providers are built only when actually iterated, so unused ones cost nothing. ## Caveats to remember - The returned object is **not thread-safe** and **caches** instances; call `reload()` to re-discover. - A misconfigured provider (missing constructor, wrong file content) throws `ServiceConfigurationError` *during iteration*, not at `load()`. - Discovery order is unspecified (roughly load order); never rely on it.

  • Does the consumer ever import the provider's concrete class?
    No. The consumer imports and depends only on the service interface; it obtains instances via ServiceLoader. That decoupling is the whole point — coupling to the concrete class would defeat the pattern.
  • Name a JDK feature built on ServiceLoader.
    JDBC driver loading (java.sql.Driver), charset providers, java.util.spi locale services, java.time chronologies, and image I/O readers are common examples.

saying these in an interview costs you the question

  • Saying ServiceLoader scans the whole classpath for implementers (it reads declared metadata, not a full scan)
  • Thinking the consumer references the concrete provider class by name (it must not)
  • Believing providers are instantiated all at once eagerly (they are lazy, per-iteration)
  • Confusing SPI with reflection-based DI frameworks like Spring — SPI is a plain JDK mechanism

context