Software systems often grow over time, and with growth comes the need for flexibility. You might want to add new features, support new formats, or integrate with new systems without modifying the core application each time. This is exactly where a plugin architecture shines.
In this post, we’ll walk through how to build a lightweight plugin architecture using Java’s Service Provider Interface (SPI). We’ll also describe a practical example involving greeting messages in multiple languages.
The main objective of this post is to make people familiarize with the plugin architecture in a simple way.
What Is a Plugin Architecture?
A Plugin Architecture is a software design pattern that allows for the core functionality of an application to be extended, modified, or customized without changing the core application’s source code.
New functionality is packaged as small, independent modules called plugins, which the main application discovers and loads at runtime.
-
Core Application (Platform): This is the stable, main part of the software. It defines a set of contracts (interfaces or abstract classes) that external components must adhere to. It provides the mechanism to discover and load extensions.
-
Plugins (Extensions): These are external, self-contained modules that implement the contracts defined by the core application. They provide the actual extension logic.
Why Use a Plugin Architecture?
Using this architecture offers several significant advantages:
-
Extensibility: New features can be added rapidly by simply deploying new plugins, without rebuilding or redeploying the entire core application.
-
Decoupling/Modularity: It separates concerns, making the core platform smaller, more stable, and easier to maintain. Plugins can be developed and updated independently.
-
Third-Party Integration: It enables third parties to build extensions for your platform (e.g., IDE extensions, browser add-ons).
-
Reduced Complexity: Allows teams to work on specific features in isolation, leading to cleaner codebases.
Real-world examples
- IntelliJ / Eclipse extensions
- Browser extensions
- Payment gateways in e-commerce platforms
What Is Java SPI (Service Provider Interface)?
Java’s SPI is a built-in mechanism that enables loose coupling between an API (interface) and its implementations. It allows an application (the core Platform) to load implementations (the Plugins) of an interface or abstract class (the Contract) that are provided by external JAR files at runtime.
It allows applications to discover implementations at runtime using simple configuration files.
At a high level, SPI involves:
-
Service: An interface or abstract class known to the application and implemented by the plugins. In the project, this is the
GreetingPlugininterface. -
Service Provider: A specific implementation of the Service (e.g.,
SpanishGreeting). -
Service Loader: A core Java class (
java.util.ServiceLoader) that the application uses to discover and load all available Service Providers for a given Service interface.
For the SPI to work:
-
Plugins must contain a special file in the
META-INF/services/directory. -
The file’s name must be the fully qualified name of the Service interface (the Contract).
- eg:
com.gintophilip.core.greeting.contract.GreetingPlugin
- eg:
-
The content of this file is a list of the fully qualified class names of the Service Provider implementations within that JAR.
com.gintophilip.spanishgreeting.SpanishGreeting com.gintophilip.hindigreeting.HindiGreeting
Project Structure and Implementation Overview
The project is divided into three modules,
-
Plugin Platform (Core Application)
-
Plugin Platform Contracts (Interfaces / Contracts)
-
Plugin Implementations (Actual Plugins)
Plugin Platform (Core Application)
This is the main application. Its primary task is to do the following:
-
Manage users (simulated database with hardcoded users)
-
Provide a CLI interface (Login, show user details, quit)
-
Use the
ServiceLoaderto discover and load the availableGreetingPluginimplementations at runtime. -
Delegate greeting responsibility to the appropriate plugin based on user language
Key Responsibilities:
-
Configuration: Reads the configured plugin folder location (e.g.,
plugins/). -
Plugin Discovery and Loading: At startup, it needs to find the plugin JARs in the folder and use
ServiceLoaderto load all implementations ofGreetingPlugin.- The core component for using the plugins, like
UserGreeter, will utilizeServiceLoader.load(GreetingPlugin.class)to get an iterable of all available plugin instances.
- The core component for using the plugins, like
-
Plugin Selection and Execution (The
UserGreeterLogic):- When a user logs in, the
UserGreeterlogic fetches the user’s preferred language (currentUser.getPreferredLanguage()). - It iterates through the loaded
GreetingPlugininstances. - It finds the plugin where
plugin.getLanguage()matches the user’s preferred language. - If a match is found, it calls
plugin.greet(userName). - If no matching plugin is found (or no preferred language is set), it falls back to the default English greeting implementation provided by the core platform itself.
- When a user logs in, the
-
CLI Interface: Provides the basic interaction loop (
Lfor login,Dfor details,q!to quit).
Plugin Platform Contracts (The Service)
This module defines the public interface that all plugins must implement. It’s the Service in the SPI pattern. It must be packaged as a JAR and be a dependency for both the Core Platform and the Plugin Implementations.
Contract Interface: GreetingPlugin
public interface GreetingPlugin {
void greet(String userName);
String getLanguage();
}
This contract defines:
Plugin Implementations
This project contains concrete plugin classes in our case, a Spanish and a Hindi greeter. These are the actual, deployable extensions. They depend only on the Contracts module. They do not need to know anything about the Platform module.
Each plugin:
-
Depends on Plugin Platform Contracts
-
Implements
GreetingPlugin -
Has a provider configuration file
In this project we provide two implemntations→ SpanishGreeting and HindiGreeting Both implementations are provided in a single JAR.
The plugin JAR must contain the following file structure and content:
-
Location:
resources/META-INF/services/ -
File Name:
com.gintophilip.core.greeting.contract.GreetingPlugin(using the fully qualified name of the contract interface). -
File Content: (Listing all implementations in the JAR)
com.gintophilip.core.greeting.plugin.SpanishGreeting com.gintophilip.core.greeting.plugin.HindiGreeting
Once packaged as a JAR and dropped into the plugins/ folder, the platform picks them up automatically at startup.
Here is a gist of the code
The contract
public interface GreetingPlugin {
void greet(String userName);
String getLanguage();
}
The implementation
public class SpanishGreeting implements GreetingPlugin {
@Override
public void greet(String userName) {
System.out.println("Hola "+ userName+" "+"bienvenido");
}
@Override
public String getLanguage() {
return "Spanish";
}
}
Loading the plugin
public void loadPlugins() {
loadDefaultPlugin();
File pluginDir = new File(PLUGIN_DIRECTORY);
if (!pluginDir.exists() || !pluginDir.isDirectory()) {
System.out.println("[PluginRepository] Plugin directory not found: " + pluginDir.getAbsolutePath());
return;
}
File[] jarFiles = pluginDir.listFiles(new JarFileFilter());
if (jarFiles == null || jarFiles.length == 0) {
System.out.println("[PluginRepository] No plugin JARs found in: " + pluginDir.getAbsolutePath());
return;
}
for (File jarFile : jarFiles) {
loadPluginFromJar(jarFile);
}
if (greetingPluginsMap.isEmpty()) {
System.out.println("[PluginRepository] No valid GreetingPlugins loaded.");
}
}
private void loadPluginFromJar(File jarFile) {
try {
URL jarUrl = jarFile.toURI().toURL();
try (URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl}, this.getClass().getClassLoader())) {
ServiceLoader<GreetingPlugin> serviceLoader =
ServiceLoader.load(GreetingPlugin.class, classLoader);
for (GreetingPlugin plugin : serviceLoader) {
addPlugin(plugin);
System.out.println("[PluginRepository] Loaded plugin: " + plugin.getClass().getName());
}
}
} catch (MalformedURLException e) {
System.err.println("[PluginRepository] Invalid plugin URL: " + jarFile.getAbsolutePath());
} catch (Exception e) {
System.err.println("[PluginRepository] Failed to load plugin from: " + jarFile.getAbsolutePath());
e.printStackTrace();
}
}
The code for each module is available in github:
How to run this project:
Clone the three repositories above. Then,
-
Build the Plugin Platform Contracts module.
After the build completes, a JAR file named
PluginPlatformContracts-1.0-SNAPSHOT.jarwill appear in thebuildfolder. -
Add the contracts JAR to both the Plugin Platform and Plugin Implementation projects.
In each project’s
build.gradle, add the following dependency:implementation files('libs/PluginPlatformContracts-1.0-SNAPSHOT.jar') -
Ensure that a
libsfolder exists in each project; create it if necessary. -
Build the Plugin Implementation module.
This will generate a JAR file named
SPHNGreeting-1.0-SNAPSHOT.jarin itsbuildfolder. -
Deploy the plugin.
Copy the generated
SPHNGreeting-1.0-SNAPSHOT.jarinto thepluginsfolder of the Plugin Platform.The location of the plugin directory is configured in the variable
PLUGIN_DIRECTORYin thePluginRepositoryclass. You can change the value here
-
Run the Plugin Platform application.
Launch
Main.javain the Plugin Platform. Once the application starts and the CLI becomes visible, typeL, then enter a username and password for a user. You should see the greeting produced by the plugin.
Here is the hard coded user details:
| First Name | Last Name | Username | Password | Preferred Language |
|---|---|---|---|---|
| John | Doe | jdoe | password123 | NONE |
| Alice | Smith | asmith | alicePass | Hindi |
| Bob | Johnson | bjohnson | securePass | NONE |
| Emma | Brown | ebrown | hello123 | Spanish |
| Liam | Wilson | lwilson | mypassword | NONE |
Each user will be greeted in following language as listed in the below table
| Username | Preferred Language |
|---|---|
| jdoe | English. [NONE is given defaults to English] |
| asmith | Hindi |
| bjohnson | English. [NONE is given defaults to English] |
| ebrown | Spanish |
| lwilson | English. [NONE is given defaults to English] |
How Plugins Are Packaged and Deployed
To deploy a plugin:
-
Build the plugin project into a JAR
-
Ensure it contains:
- implementation classes
-
META-INF/services/...provider file - The contract dependency
-
Copy the JAR into the configurable plugin directory
-
Restart the core application
-
The new language is now supported with no code changes to the core app
Happy Coding and Exploring Plugins!🎉
Remember, building plugin architectures is not just about adding features, it’s about designing software that can grow gracefully over time. Every new plugin you create is a small step toward mastering modular, flexible, and maintainable Java applications. Keep experimenting, keep learning, and enjoy the journey!



