DEV Community

Solon Framework
Solon Framework

Posted on

Hot-Plug in Practice: Managing Solon Plugins at Runtime

Every time a business module needs an update, do you have to restart the whole application? For small services that is a minor annoyance. Once you are running a monolith with a dozen integration modules, or a gateway with region-specific routing rules, every restart means a disruption window, a connection drain, and a small ceremony around "when is a safe moment to do it".

Solon's answer to this class of problem is solon-hotplug — a base extension module that gives business plugins hot-plug and hot-management support. "Hot" here means exactly what you hope it means: you update an extension package without restarting the main program, and you manage the lifecycle through an interface (or an HTTP endpoint, or a database-driven admin panel). The framework documentation is honest about the trade-off though: hot pluggability brings new development constraints. So the goal of this post is to show you how it works, what those constraints are, and when you should (and should not) reach for it.

First, where does this fit?

Before we dive in, a quick positioning note, because it saves a lot of confusion. In an earlier post I covered Solon's two SPI-style extension mechanisms — E-Spi (external extension, jars dropped in and discovered at startup) and H-Spi (hot-plug). The official guidance is deliberately conservative:

In normal cases, use the ordinary external extension mechanism (E-Spi).

solon-hotplug is the module that implements the H-Spi side. You reach for it when you genuinely need to load, start, stop and unload plugins while the main process keeps running. If your extension packages are fixed at deploy time, E-Spi is simpler and has fewer constraints. Hot-plug is a capability you pay for with discipline — and that discipline is exactly what this post is about.

The base API: load and unload a jar

The lowest-level interface is PluginPackage. It is the foundation, but the docs note that you normally do not call it directly — you use the management API instead. Still, seeing it makes everything else obvious:

public class DemoApp {
    public static void main(String[] args) {
        Solon.start(Test5App.class, args);

        File jarFile = new File("/xxx/xxx.jar");

        // load the plugin and start it
        PluginPackage jarPlugin = PluginPackage.loadJar(jarFile).start();

        // unload the plugin
        PluginPackage.unloadJar(jarPlugin);
    }
}
Enter fullscreen mode Exit fullscreen mode

loadJar reads the jar, start() boots its plugin, unloadJar tears it down. Simple. But if you want to manage several plugins by name — which is what a real admin surface needs — you move up to PluginManager.

Hot management by name

The management model is: give each plugin a name, point it at a jar path, then drive its lifecycle with load / start / stop / unload. You can declare the registry in configuration:

solon.hotplug:
  add1: "/x/x/x.jar"   # format: name: jarfile
  add2: "/x/x/x2.jar"
Enter fullscreen mode Exit fullscreen mode

Or register plugins from code — and because it is just code, nothing stops you from reading the registry from a database and building a platform out of it:

PluginManager.add("add1", "/x/x/x.jar");
PluginManager.add("add2", "/x/x/x2.jar");
// PluginManager.remove("add2"); // remove a plugin from management
Enter fullscreen mode Exit fullscreen mode

Then the lifecycle calls become trivial to expose. Here is a minimal example from the docs that starts and stops a plugin over HTTP:

public class App {
    public static void main(String[] args) {
        Solon.start(App.class, args, app -> {
            // start a plugin
            app.router().get("start", ctx -> {
                PluginManager.start("add1");
                ctx.output("OK");
            });

            // stop a plugin
            app.router().get("stop", ctx -> {
                PluginManager.stop("add1");
                ctx.output("OK");
            });
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Two behaviors are worth remembering because they make the API forgiving:

  • start("add2") auto-loads the plugin if it is not loaded yet.
  • unload("add2") auto-stops the plugin first if it is still running.

So the four operations compose safely: you can stop a running plugin, swap its jar, and start it again — without the main application ever going down.

The discipline: what a well-behaved plugin must clean up

Here is the part that actually decides whether hot-plug works in production. When you stop a plugin, the framework cannot know which routes, jobs, listeners or static resources the plugin registered — only the plugin knows. The official docs put it bluntly: compared to an ordinary plugin, a hot-pluggable plugin must remove the resources it registered during preStop or stop. This is very important.

The canonical example unregisters four kinds of resources on stop:

public class Plugin1Impl implements Plugin {
    AppContext context;
    StaticRepository staticRepository;

    @Override
    public void start(AppContext context) {
        this.context = context;

        // scan this plugin's own components
        this.context.beanScan(Plugin1Impl.class);

        // register its own static files
        staticRepository = new ClassPathStaticRepository(context.getClassLoader(), "plugin1_static");
        StaticMappings.add("/", staticRepository);
    }

    @Override
    public void stop() throws Throwable {
        // remove http handlers (prefix-based, so removal is easy)
        Solon.app().router().remove("/user");

        // remove scheduled jobs
        JobManager.remove("job1");

        // remove event subscriptions
        context.beanForeach(bw -> {
            if (bw.raw() instanceof EventListener) {
                EventBus.unsubscribe(bw.raw());
            }
        });

        // remove the static file repository
        StaticMappings.remove(staticRepository);
    }
}
Enter fullscreen mode Exit fullscreen mode

Four categories, each with a matching teardown call:

What the plugin registered on start What it must do on stop
Component scan via context.beanScan(...) iterate beans, unsubscribe EventListeners via EventBus.unsubscribe
Static files via StaticMappings.add("/", repo) StaticMappings.remove(repo)
HTTP routes Solon.app().router().remove("/user") (use a prefix to remove whole groups)
Scheduled jobs JobManager.remove("job1")

Notice the asymmetry: routes and jobs are removed by name/prefix (so keep your prefixes tidy), while event listeners are removed by iterating your own beans. If a plugin forgets one of these, the first hot-swap looks fine — and the second one starts leaking behavior from a ghost plugin. The stop method is not optional polish; it is the contract that makes hot-plug safe.

The constraints: how to package a plugin that can be "pulled out"

Because hot-plug wants a plugin to be domain-independent — to barely interact with anything else, so that its resources can be pulled out cleanly — the docs impose three packaging rules:

1. Package names must be independent. Otherwise the component scanner can pick up classes from the wrong plugin. The convention: main app uses xxx or xxx.main, plugin 1 uses xxx.add1, plugin 2 uses xxx.add2.

2. Dependencies are placed deliberately. Shared/public dependencies go into the main program package (this keeps plugin jars smaller); dependencies that must be isolated go inside the plugin package.

3. The plugin reaches the main program through the container, not by static state. Get main-program beans via Solon.context().getBean(...), and main-program configuration via Solon.cfg(). That keeps the dependency direction clean: plugin → container → main resources, never plugin → plugin internals.

These three rules are the practical price of "hot". They are not hard to follow, but they shape how you design the boundary between the main app and its plugins from day one.

Choosing between E-Spi and solon-hotplug

A pragmatic way to think about it: your extension needs live on a spectrum.

  • Cold (E-Spi): jars placed in an external directory, discovered and wired at startup. Zero extra constraints, perfectly fine for the majority of cases. If a change requires a deploy anyway, this is the honest default.
  • Hot (solon-hotplug): jars managed by name at runtime, load/start/stop/unload through code or an admin surface. You pay with the packaging discipline above and the stop-cleanup contract.

The official docs point out that hot-plug plugins should stay as domain-independent as possible, and suggest pairing with DamiBus to help decouple — worth keeping in mind when you design the plugin boundary. And if you want to see a complete, runnable example instead of snippets, the official repository has a three-module demo: demo1011-hotplug_common, demo1011-hotplug_main and demo1011-hotplug_plugin1 under solon-examples/1.Solon/.

Wrapping up

solon-hotplug gives Solon applications a real runtime plugin lifecycle: PluginPackage for one-off load/unload, PluginManager for named, managed hot-swaps, and a clear contract for plugin authors — register on start, remove everything on stop. The capability is genuinely useful for gateways, region-aware modules, and long-running services where a restart is a production event.

But the honest takeaway is the one the docs lead with: use E-Spi for normal extension cases, and reach for hot-plug only when you actually need runtime management. The framework makes the hot path available without bloating the cold path — which, for a framework whose philosophy is restraint, is exactly the trade you want to be offered.

This post is based on the official Solon documentation (v4.0.4). Check solon.noear.org for the current version and the full reference.

Top comments (0)