DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • In-Depth Guide to Using useMemo() Hook in React
  • Smart Dependency Injection With Spring: Assignability (Part 2 of 3)
  • Automating Cucumber Data Table to Java Object Mapping in Your Cucumber Tests
  • The Hidden Costs of Lombok in Enterprise Java Solutions

Trending

  • Why Stable RAG Answers Can Still Hide Unstable Evidence
  • Jakarta EE 12: Entering the Data Age of Enterprise Java
  • Alternative Structured Concurrency
  • RAG Is Not Enough: Advanced Retrieval Architectures Using Vertex AI Search on GCP
  1. DZone
  2. Coding
  3. Languages
  4. Extending Java Libraries with Service Loader

Extending Java Libraries with Service Loader

How to dynamically use Java’s Service Loader to discover and load SPI implementations at runtime for plugin-like extensions.

By 
Dominik Przybysz user avatar
Dominik Przybysz
·
Mar. 13, 26 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
3.6K Views

Join the DZone community and get the full member experience.

Join For Free

When designing a Java library, extensibility is often a key requirement, especially in the later phases of a project. Library authors want to allow users to add custom behavior or provide their own implementations without modifying the core codebase. Java addresses this need with the Service Loader API, a built-in mechanism for discovering and loading implementations of a given interface at runtime.

Service Loader enables a clean separation between the Application Programming Interface (API) and its implementation, making it a solid choice for plugin-like architectures and Service Provider Interfaces (SPI). In this post, we’ll look at how Service Loader can be used in practice, along with its advantages and limitations when building extensible Java libraries.

Example Usage

In the demo project, the library allows customization of the naming strategy based on annotations, for which dedicated SPI implementations are provided.

SPI Definition

First, let’s start with the SPI in the core library module:

Java
 
public interface TypeAliasHandler<T extends Annotation> {
    Class<T> getSupportedAnnotation();

    String getTypeName(T annotation, Class<?> annotatedClass);
}


To enable the Service Loader API to discover implementations of this interface, a configuration file must be created in the META-INF/services/ directory on the classpath. The file name must exactly match the fully qualified name of the interface. Inside this file, list the fully qualified class names of all implementing classes, one per line. This mechanism allows Service Loader to automatically find and load all available implementations at runtime.

Built-in Providers

Within the same JAR file, we can define built-in annotations and their default behavior. For architectural consistency and convenience, the handler responsible for the built-in annotation also implements the SPI interface. This approach ensures that both internal and external implementations are treated uniformly by the Service Loader mechanism.

Java
 
public class BuiltInTypeAliasHandler implements TypeAliasHandler<TypeAlias> {
    @Override
    public Class<TypeAlias> getSupportedAnnotation() {
        return TypeAlias.class;
    }

    @Override
    public String getTypeName(TypeAlias annotation, Class<?> annotatedClass) {
        return annotation.value();
    }
}


The annotation is:

Java
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TypeAlias {
    String value();
}


The implementation must be defined in:

Plain Text
 
META-INF/services/com.github.alien11689.serviceloaderdemo.coreservice.spi.TypeAliasHandler


with the following content:

Plain Text
 
com.github.alien11689.serviceloaderdemo.coreservice.builtin.BuiltInTypeAliasHandler


Extensions Module

You can create a separate project (or JAR file) that provides custom annotations and their implementations. Such an extension module can be developed independently from the main library and added to the classpath as needed.

This demonstrates the true power of Service Loader — the ability to add new functionality without modifying the main library’s source code. No recompilation or redeployment of the core library is required.

Let’s start with the annotations:

Java
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustomTypeAlias {
    String nameOfTheType();
}


and

Java
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface UpperCasedClassSimpleNameTypeAlias {
}


Their handlers (SPI implementations):

Java
 
@ServiceProvider
public class CustomTypeAliasHandler implements TypeAliasHandler<CustomTypeAlias> {
    @Override
    public Class<CustomTypeAlias> getSupportedAnnotation() {
        return CustomTypeAlias.class;
    }

    @Override
    public String getTypeName(CustomTypeAlias annotation, Class<?> annotatedClass) {
        return annotation.nameOfTheType();
    }
}


and

Java
 
@ServiceProvider
public class UpperCasedClassSimpleNameTypeAliasHandler implements TypeAliasHandler<UpperCasedClassSimpleNameTypeAlias> {
    @Override
    public Class<UpperCasedClassSimpleNameTypeAlias> getSupportedAnnotation() {
        return UpperCasedClassSimpleNameTypeAlias.class;
    }

    @Override
    public String getTypeName(UpperCasedClassSimpleNameTypeAlias annotation, Class<?> annotatedClass) {
        return annotatedClass.getSimpleName().toUpperCase();
    }
}


Since I used the @ServiceProvider annotation available from Avaje, I do not need to create the META-INF/services/...TypeAliasHandler file manually. It is generated automatically during the build with the following content:

Plain Text
 
com.github.alien11689.serviceloaderdemo.extensions.custom.CustomTypeAliasHandler
com.github.alien11689.serviceloaderdemo.extensions.uppercased.UpperCasedClassSimpleNameTypeAliasHandler


Discovering the Implementation

In one of the modules (even the one providing the SPI), there should be code that uses the Service Loader API to discover all implementations and use them. In this example, I placed the discovery code in the core module, which is a practical approach — the central module can aggregate all available implementations and provide convenient access to the rest of the application.

In the static initialization block, Service Loader scans the entire classpath for configuration files and automatically creates instances of all discovered implementations:

Java
 
public class TypeAliasProvider {

    private static Map<Class<? extends Annotation>, TypeAliasHandler> annotationToTypeNameHandler = new HashMap<>();

    static {
        var loader = ServiceLoader.load(TypeAliasHandler.class);
        loader.forEach(typeNameHandler -> annotationToTypeNameHandler.put(typeNameHandler.getSupportedAnnotation(), typeNameHandler));
    }

    // ...
}


In the same class, the discovered implementations can then be used based on the annotations present on a given class:

Java
 
public class TypeAliasProvider {
    // ...

    public String getTypeName(Object o) {
        var aClass = o.getClass();
        for (Annotation annotation : aClass.getAnnotations()) {
            var typeNameHandler = annotationToTypeNameHandler.get(annotation.annotationType());
            if (typeNameHandler != null) {
                return typeNameHandler.getTypeName(annotation, aClass);
            }
        }
        return aClass.getName();
    }
}


Let’s Test It Together

To test the extension mechanism effectively, all SPI implementations must be available on the classpath. This means you need to include both the core module (with the SPI definition) and all extension modules containing specific implementations in the test project. Service Loader will automatically discover all available services and enable their use during test execution.

Test classes:

Java
 
@TypeAlias("class_a")
class ClassWithDefaultTypeAlias {
}

@CustomTypeAlias(nameOfTheType = "Class B with custom alias")
class ClassWithCustomTypeAlias {
}

@UpperCasedClassSimpleNameTypeAlias
class UpperCaseClass {
}


Parameterized test:

Java
 
class TypeAliasExtensionMappingTest {

    private final TypeAliasProvider typeAliasProvider = new TypeAliasProvider();

    @ParameterizedTest
    @MethodSource("objectToTypeName")
    void should_map_object_to_type_name(Object o, String expectedTypeName) {
        Assertions.assertEquals(expectedTypeName, typeAliasProvider.getTypeName(o));
    }

    private static Stream<Arguments> objectToTypeName() {
        return Stream.of(
                arguments(new Object(), "java.lang.Object"),
                arguments(new ClassWithDefaultTypeAlias(), "class_a"),
                arguments(new ClassWithCustomTypeAlias(), "Class B with custom alias"),
                arguments(new UpperCaseClass(), "UPPERCASECLASS")
        );
    }
}


Full Code

The full sample code can be found on my GitHub. The demo was initially designed to demonstrate extension possibilities for Javers.

Pros

  • Lightweight and dependency-free – Service Loader is part of the JDK and requires no additional runtime libraries.
  • Standardized solution – Works consistently across all JVM environments.
  • Automatic service discovery – Implementations are discovered at runtime without explicit registration in code.
  • Decoupled architecture – Encourages clean separation between core and plugins.

Cons

  • No constructor arguments – Service implementations must provide a no-argument constructor, making configuration and dependency passing difficult. Additional SPI methods may be necessary (e.g., void configure(Properties properties)).
  • No built-in dependency injection – Service Loader does not manage dependencies, scopes, or lifecycle.
  • Public class requirement – Implementations must be declared as public, which limits encapsulation.
  • Limited configurability – Conditional or environment-based service loading is not supported out of the box.
  • Harder to debug – Missing or incorrect service definitions may fail silently at runtime.
  • Not ideal for complex systems – For advanced use cases, full DI frameworks such as Spring or Guice offer more flexibility.

Summary

Service Loader is a simple yet powerful tool for building extensible Java libraries. It excels in scenarios where minimal dependencies, portability, and clear API boundaries are important. While it has notable limitations — particularly around constructor flexibility, dependency injection, and visibility constraints — it remains an excellent choice for lightweight extension mechanisms.

With the help of tools like Avaje, some of the traditional pain points of Service Loader can be reduced, making it an even more attractive option for modern Java library design.

API Annotation Dependency injection Implementation Library Service provider interface Interface (computing) Java (programming language) Loader (equipment) Data Types

Published at DZone with permission of Dominik Przybysz. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • In-Depth Guide to Using useMemo() Hook in React
  • Smart Dependency Injection With Spring: Assignability (Part 2 of 3)
  • Automating Cucumber Data Table to Java Object Mapping in Your Cucumber Tests
  • The Hidden Costs of Lombok in Enterprise Java Solutions

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook