Interfaces & Abstract Classes

Project: A Plugin System with Interfaces

15 min Lesson 10 of 14

Project: A Plugin System with Interfaces

Throughout this tutorial you have studied interfaces as a language feature. In this final lesson you apply everything by designing a small but realistic plugin system — a pattern used in IDEs, game engines, build tools, and countless frameworks. The goal is to write an application core that can be extended at runtime without modifying existing code, a direct application of the Open/Closed Principle.

What a Plugin System Is — and Why It Matters

A plugin system separates the host (the stable application core) from plugins (independently written extensions). The host knows only about an interface contract; each plugin implements that contract. Neither side needs to know the other's internals.

This gives you three concrete benefits:

  • Extensibility without modification — adding a new plugin never touches the host.
  • Testability — you can inject a mock plugin during unit tests.
  • Swappability — swap implementations (e.g. switch from a file-based exporter to a cloud-based one) without rewriting business logic.
This is also called the Strategy Pattern. When the "plugin" is chosen at runtime and encapsulates an algorithm, you are implementing the classic Strategy pattern from the Gang of Four. Interfaces are the natural tool for it in Java.

Step 1 — Define the Plugin Contract

The first design decision is the interface every plugin must satisfy. Keep it focused: one interface, one responsibility.

package plugins; /** * Contract every report-exporter plugin must fulfil. */ public interface ReportExporter { /** A short, human-readable name shown in menus. */ String name(); /** * Export the given rows to some destination. * @param headers column names * @param rows table data (each inner list is one row) */ void export(List<String> headers, List<List<String>> rows); }

Two methods, a clear contract. The host only depends on ReportExporter — never on any concrete class.

Step 2 — Implement Several Plugins

Each plugin is a separate class that implements the interface. Here are three concrete implementations.

package plugins; import java.util.List; import java.util.stream.Collectors; /** Writes a plain-text CSV to System.out (or a real file in production). */ public class CsvExporter implements ReportExporter { @Override public String name() { return "CSV Export"; } @Override public void export(List<String> headers, List<List<String>> rows) { System.out.println(String.join(",", headers)); for (List<String> row : rows) { System.out.println(String.join(",", row)); } } }
package plugins; import java.util.List; /** Renders an ASCII table to the console. */ public class ConsoleTableExporter implements ReportExporter { @Override public String name() { return "Console Table"; } @Override public void export(List<String> headers, List<List<String>> rows) { String separator = "+-----------+-----------+-----------+"; System.out.println(separator); System.out.printf("| %-9s | %-9s | %-9s |%n", headers.get(0), headers.get(1), headers.get(2)); System.out.println(separator); for (List<String> row : rows) { System.out.printf("| %-9s | %-9s | %-9s |%n", row.get(0), row.get(1), row.get(2)); } System.out.println(separator); } }
package plugins; import java.util.List; /** Produces a minimal HTML table string. */ public class HtmlExporter implements ReportExporter { @Override public String name() { return "HTML Export"; } @Override public void export(List<String> headers, List<List<String>> rows) { StringBuilder sb = new StringBuilder("<table>\n<tr>"); for (String h : headers) sb.append("<th>").append(h).append("</th>"); sb.append("</tr>\n"); for (List<String> row : rows) { sb.append("<tr>"); for (String cell : row) sb.append("<td>").append(cell).append("</td>"); sb.append("</tr>\n"); } sb.append("</table>"); System.out.println(sb); } }

Step 3 — Build the Plugin Registry

The host maintains a registry: a Map keyed by plugin name. Plugins register themselves; the host looks them up by name when the user makes a choice.

package plugins; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; public class PluginRegistry { private final Map<String, ReportExporter> plugins = new LinkedHashMap<>(); /** Register a plugin. Overwrites if the name already exists. */ public void register(ReportExporter plugin) { plugins.put(plugin.name(), plugin); } /** Retrieve a plugin by its name, or null if not found. */ public ReportExporter get(String name) { return plugins.get(name); } /** All registered plugin names, in insertion order. */ public Collection<String> availablePlugins() { return plugins.keySet(); } }
Favour composition over inheritance here. The registry holds references to plugin objects rather than extending some abstract class. This keeps the host completely decoupled: it does not care which class a plugin comes from, only that it satisfies ReportExporter.

Step 4 — Wire It All Together in the Host

The main application class registers the available plugins and delegates to whichever the user picks. Notice that the selection logic and the export logic are completely separate.

package plugins; import java.util.List; import java.util.Scanner; public class ReportApp { public static void main(String[] args) { // --- Build the registry --- PluginRegistry registry = new PluginRegistry(); registry.register(new CsvExporter()); registry.register(new ConsoleTableExporter()); registry.register(new HtmlExporter()); // --- Sample data --- List<String> headers = List.of("Name", "Role", "Score"); List<List<String>> rows = List.of( List.of("Alice", "Dev", "92"), List.of("Bob", "Design", "88"), List.of("Carol", "QA", "95") ); // --- User picks an exporter --- System.out.println("Available exporters: " + registry.availablePlugins()); Scanner scanner = new Scanner(System.in); System.out.print("Choose exporter: "); String choice = scanner.nextLine().trim(); ReportExporter exporter = registry.get(choice); if (exporter == null) { System.out.println("Unknown exporter: " + choice); return; } // --- Delegate entirely to the plugin --- exporter.export(headers, rows); } }

To add a new export format — say JSON — you create one new class that implements ReportExporter and add one register call. Zero lines inside ReportApp or any existing plugin change.

Step 5 — Adding a Default Method to the Contract

Interfaces can carry default methods, which is useful for optional behaviour that most plugins share. Suppose every exporter should be able to describe itself:

public interface ReportExporter { String name(); void export(List<String> headers, List<List<String>> rows); /** Optional: human-readable description. Override when needed. */ default String description() { return "No description provided for " + name(); } }

Existing plugins compile with no changes. Only the plugins that want a custom description override description(). This is how Java evolves APIs without breaking existing implementations.

Do not abuse default methods. They are designed for API evolution (adding behaviour to an existing interface without a breaking change), not as a shortcut to avoid writing implementations. If every plugin truly needs its own behaviour, keep the method abstract.

Design Recap

The full system uses only three interface features you have studied:

  1. Abstract methods (name(), export()) — the mandatory contract.
  2. Default methods (description()) — optional enhancements with a sensible fallback.
  3. Polymorphism — the host works through the ReportExporter reference; the runtime type (CSV, HTML, …) is resolved automatically.

This pattern scales from two plugins to two hundred. Frameworks like Spring, IntelliJ IDEA, and Maven all use it extensively. Mastering it is a fundamental step toward writing professional, extensible Java code.

Summary

A plugin system is an interface contract plus a registry. The host defines the contract, each plugin implements it independently, and the registry connects them at runtime. Adding a plugin never modifies the host — that is the power of programming to an interface rather than an implementation.