In the vast realm of Java programming, developers are often faced with the challenge of creating modular, extensible, and maintainable applications. As software systems grow in complexity, the need to decouple components and facilitate dynamic plugin loading becomes paramount. This is where the Java ServiceLoader
API shines, offering a robust and elegant solution for achieving component-based architecture and runtime extensibility. While the ServiceLoader API has been part of Java since version 6, its popularity and user-friendliness truly soared with the advent of Java 9’s module system.
In this blog post, we will dive into the world of the ServiceLoader
API by exploring a simple yet illustrative example involving a fictional “CoffeeVendor” service. We’ll demonstrate how to leverage the ServiceLoader API to effortlessly discover and incorporate different coffee provider implementations into our CoffeeVendor application. Through this example, we will showcase the power and simplicity of the ServiceLoader API and highlight its significance in building adaptable and pluggable Java applications.
So, grab your virtual coffee cup, and let’s embark on a journey to uncover the capabilities of the ServiceLoader API and how it can enhance the modularity and extensibility of your Java projects.
Defining the CoffeeVendor Service
To set the stage, we’ll introduce our central character: the CoffeeVendor service. This service, a coveted gem for client applications, adheres to a concise interface:
public interface CoffeeVendor {
String getName();
List<String> getVariants();
}
This integral interface resides within the “coffee.vendor” module:
module coffee.vendor {
exports info.sanaulla.coffeevendor;
}
A Glimpse of the Module Structure
Before delving deeper, let’s take a quick glance at the module structure as envisioned within the IntelliJ IDE:
Enlisting the CoffeeVendor Client
With our stage well-set, it’s time to introduce, the “Client.” This class serves as the conduit to various implementations, aptly referred to as service providers, of the CoffeeVendor
service. Leveraging the ServiceLoader.load() method, our Client eagerly loads all CoffeeVendor interface implementations available within the classpath/modulepath at runtime
public class Client {
public static void main(String[] args) {
ServiceLoader<CoffeeVendor> coffeeVendors = ServiceLoader.load(CoffeeVendor.class);
System.out.println("Found the following coffee vendors");
for(CoffeeVendor coffeeVendor : coffeeVendors){
System.out.println(coffeeVendor.getName() + ". Variants: " + coffeeVendor.getVariants());
}
}
}
The Client’s home is the “client” module:
module client {
requires coffee.vendor;
uses info.sanaulla.coffeevendor.CoffeeVendor;
}
A noteworthy detail: adding the line “uses <name of the service interface>” within our module descriptor is vital for ServiceLoader’s operation. Without this inclusion, a runtime error occurs:
Clik here to view.

We can execute our client without providing any service provider. You will see just the below printed:
Found the following coffee vendors
Unveiling CoffeeVendor Service Providers
Let us provide two implementations for CoffeeVendor
service: CostaCoffee
and Starbucks
.
CostaCoffee
service provider implementation is given below:
public class CostaCoffee implements CoffeeVendor {
public CostaCoffee(){
System.out.println("Costa Coffee created");
}
@Override
public String getName() {
return "Costa Coffee";
}
@Override
public List<String> getVariants() {
return Arrays.asList("Latte", "Flat Black", "Cortado", "Espresso", "Mocha");
}
}
It is part of the module costcoffee
whose module definition is given below:
module costacoffee {
requires coffee.vendor;
provides info.sanaulla.coffeevendor.CoffeeVendor
with info.sanaulla.costacoffee.CostaCoffee;
}
The important piece of code above is provides ... with ...
which shows that this module provides an implementation for the service CoffeeVendor
.
Starbucks
service provider implementation is given below:
public class Starbucks implements CoffeeVendor {
public Starbucks(){
System.out.println("Starbucks created");
}
@Override
public String getName() {
return "Starbucks";
}
@Override
public List<String> getVariants() {
return Arrays.asList("Latte", "Espresso", "Cappuccino",
"Caffe Americano", "Flat White" );
}
}
It is part of the module starbucks
whose module definition is given below:
module starbucks {
requires coffee.vendor;
provides info.sanaulla.coffeevendor.CoffeeVendor
with info.sanaulla.starbucks.Starbucks;
}
The modules directory structure looks something like below:
Conclusion
I am using IntelliJ IDE for this sample. So if I run my Client
main class to my surprise I will not find any of the service providers we defined above. This is because these service provider modules are not part of the runtime dependency. So we need to add them as part of runtime dependency from the Module Settings as shown below:
Clik here to view.

If we run the main class now you will be able to see the following output:
The complete sample can be found in Github repo here.
The post Unveiling the Magic of Java ServiceLoader API: Building Modular and Extensible Applications appeared first on Experiences Unlimited.