Using Services in Java Modules: provides and uses Explained with Real Examples

Illustration for Using Services in Java Modules: provides and uses Explained with Real Examples
By Last updated:

One of the biggest pain points developers face when modularizing applications is decoupling components. On the classpath, everything is accessible globally, making it easy but messy. With JPMS, dependencies must be explicitly declared, which leads to a new question: how do modules communicate without creating tight coupling?

The answer lies in Java’s Service Loader mechanism combined with the provides and uses directives. This system allows one module to define a service API, other modules to provide implementations, and clients to consume services dynamically—all while preserving encapsulation.

This tutorial explores how to use provides and uses to design extensible, loosely coupled modular applications.


Step 1: Define a Service Interface

Let’s start with a service API module:

// file: src/com.example.service/module-info.java
module com.example.service {
    exports com.example.service.api;
}
// file: src/com.example.service/com/example/service/api/MessageService.java
package com.example.service.api;

public interface MessageService {
    String getMessage();
}
  • MessageService defines the contract.
  • The module exports only the API package.

Step 2: Provide an Implementation

Create an implementation module that provides the service.

// file: src/com.example.english/module-info.java
module com.example.english {
    requires com.example.service;
    provides com.example.service.api.MessageService 
        with com.example.english.EnglishMessageService;
}
// file: src/com.example.english/com/example/english/EnglishMessageService.java
package com.example.english;

import com.example.service.api.MessageService;

public class EnglishMessageService implements MessageService {
    public String getMessage() {
        return "Hello, JPMS Services!";
    }
}
  • The module declares it provides an implementation.
  • JPMS ensures only declared services are visible.

Step 3: Use the Service

Now, create a consumer module:

// file: src/com.example.app/module-info.java
module com.example.app {
    requires com.example.service;
    uses com.example.service.api.MessageService;
}
// file: src/com.example.app/com/example/app/Main.java
package com.example.app;

import com.example.service.api.MessageService;
import java.util.ServiceLoader;

public class Main {
    public static void main(String[] args) {
        ServiceLoader<MessageService> loader = ServiceLoader.load(MessageService.class);
        for (MessageService service : loader) {
            System.out.println(service.getMessage());
        }
    }
}
  • The uses directive signals that the module consumes MessageService.
  • ServiceLoader discovers and loads all providers at runtime.

Step 4: Running the Application

Compile and run with the module path:

javac -d out --module-source-path src $(find src -name "*.java")
java --module-path out -m com.example.app/com.example.app.Main

Expected output:

Hello, JPMS Services!

Pitfalls & Misuse Cases

  1. Forgetting uses → The consumer won’t discover implementations.
  2. Multiple Providers Without Clarity → ServiceLoader loads all implementations, which can cause ambiguity.
  3. Relying on Classpath Behavior → Service discovery may silently fail if modules aren’t properly declared.
  4. Exporting Implementation Packages → Violates encapsulation; only APIs should be exported.

Best Practices

  • Keep service APIs and implementations in separate modules.
  • Export only the API package, not implementation classes.
  • Use provides ... with to register implementations clearly.
  • Document expected service behavior to avoid conflicts when multiple providers exist.
  • Test services with different providers to ensure flexibility.

📌 What's New in Java Versions?

  • Java 5 → N/A (modules introduced in Java 9)
  • Java 9 → JPMS introduced with provides and uses support in module descriptors
  • Java 11 → Improved tooling for debugging service loading
  • Java 17 → Performance refinements in ServiceLoader
  • Java 21 → No significant updates across Java versions for this feature

Analogy

Think of services in modules as a plug-and-socket system:

  • The socket (API module) defines the shape of the plug.
  • The plugs (implementation modules) provide different variants.
  • The consumer simply uses a socket, without caring which plug is connected.

This allows systems to be extended without modifying existing code.


Summary + Key Takeaways

  • provides registers service implementations in a module.
  • uses declares service consumption.
  • ServiceLoader enables dynamic discovery of implementations.
  • Keep APIs and implementations separate to maintain encapsulation.
  • JPMS services promote loose coupling and extensibility in modular applications.

FAQs

Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces explicit declarations.

Q2. Why do I get “package is not visible” errors when using modules?
Because the service API wasn’t exported in module-info.java.

Q3. What is the purpose of requires transitive?
It re-exports dependencies for downstream modules, but should not be confused with services.

Q4. How do open and opens differ in reflection?

  • open module opens all packages.
  • opens selectively opens specific packages for reflection.

Q5. What are automatic modules, and should I use them?
They’re non-modular JARs treated as modules. Useful during migration, but fragile in production.

Q6. How does JPMS improve security compared to classpath?
By limiting reflective access, disallowing split packages, and enforcing encapsulation.

Q7. When should I use jlink vs jmod?

  • jlink → Build custom runtime images.
  • jmod → Package and distribute modules.

Q8. Can I migrate a legacy project to services incrementally?
Yes, by modularizing APIs first and then moving providers and consumers.

Q9. How do I handle multiple service providers?
ServiceLoader returns all providers; choose one programmatically if necessary.

Q10. Do frameworks like Spring or Hibernate fully support JPMS services?
Not directly—these frameworks rely on their own mechanisms, but JPMS services can complement them.