W3docs

Java Modules: Services

Service-loader pattern in the Java module system using the uses and provides directives.

Java Modules: Services

A central goal of modules is decoupling: code that uses a capability should not name the class that implements it. JPMS turns this into a first-class feature with two directives — uses and provides — wired together at runtime by ServiceLoader. This is the modular replacement for the old classpath convention of dropping a META-INF/services file in a JAR.

The three roles

A service consists of three parts, ideally in three different modules:

  1. The service interface (or abstract class) — the contract, e.g. PricingRule. It lives in a module that exports it.
  2. The consumer — code that asks for implementations. Its module declares uses com.acme.PricingRule; and calls ServiceLoader.load(PricingRule.class).
  3. One or more providers — implementation modules. Each declares provides com.acme.PricingRule with com.acme.impl.StandardPricing;.

The consumer never imports StandardPricing. It only knows the interface. Add a new provider module to the module path and the consumer picks it up — no recompilation, no code change.

The directives in module-info.java

// module com.acme.api
module com.acme.api {
    exports com.acme;            // export the PricingRule interface
}

// module com.acme.app (the consumer)
module com.acme.app {
    requires com.acme.api;
    uses com.acme.PricingRule;   // "I will ServiceLoader.load this"
}

// module com.acme.standard (a provider)
module com.acme.standard {
    requires com.acme.api;
    provides com.acme.PricingRule
        with com.acme.standard.StandardPricing;
}

uses tells the resolver the consumer will look up that service, so the module is allowed to call ServiceLoader.load. provides … with … registers an implementation. The provider class must either have a public no-arg constructor or a public static provider() method that returns an instance.

Consuming with ServiceLoader

ServiceLoader<PricingRule> loader = ServiceLoader.load(PricingRule.class);
for (PricingRule rule : loader) {
    System.out.println(rule.describe());
}
// or pick the first available
PricingRule rule = ServiceLoader.load(PricingRule.class)
    .findFirst()
    .orElseThrow();

ServiceLoader is lazy — each provider is instantiated only when the iterator reaches it — and it caches instances. It implements Iterable, so a for-each loop walks every registered provider.

A worked example: ServiceLoader over a real JDK service

The JDK already ships a service you can load without building three modules: java.util.spi.ToolProvider. The compiler (javac), the JAR tool (jar), and others are registered as providers of that interface inside JDK modules. This program loads them through ServiceLoader — exactly the consumer code above, against a service that is already wired up.

java— editable, runs on the server

What to take from the run:

  • The for-each loop walked every ToolProvider the runtime registered (typically javac, jar, javadoc, …) without the program ever importing one of those implementation classes. That is the entire point of services: the consumer depends on ToolProvider, the interface, and discovers concrete providers at runtime.
  • Each provider printed a different implementation class name even though they share one interface. The JDK modules declared provides java.util.spi.ToolProvider with … in their descriptors; ServiceLoader collected them all. Adding another provider module would make it appear in this same loop with zero changes here.
  • ToolProvider.findFirst("javac") returned an Optional and the code handled both branches. Service lookups are inherently "might be absent" — a minimal runtime could ship no tool providers — so the API forces you to plan for the empty case rather than assuming an implementation exists.
  • Running javac --version through the loaded provider proves the object is fully functional, reached purely through the service contract. The consumer invoked real behaviour without a compile-time dependency on the compiler's classes.
  • ServiceLoader instantiates lazily and only what you iterate to; in a real three-module setup the consumer's module-info would need uses java.util.spi.ToolProvider; for the call to be permitted. The JDK's own modules already declare it, which is why this runs unchanged.

Why use services

  • Plugin architectures — drop a provider JAR onto the module path to extend an application.
  • Optional implementations — choose an SSL, logging, or database driver at runtime by which provider module is present.
  • Inversion of dependency — the high-level consumer module depends on an interface module, never on the low-level providers, so the dependency arrows all point at the stable contract.

This is the same mechanism the JDK uses for DriverManager, charset providers, and the java.time chronologies. The final chapter of this part puts everything together: how to migrate an existing classpath application onto the module path one step at a time.

Practice

Practice

A consumer module calls 'ServiceLoader.load(PaymentGateway.class)' but the loop finds zero providers at runtime, even though a provider module declaring 'provides PaymentGateway with StripeGateway' is on the module path. The consumer module compiles and starts fine. What is the most likely cause?