Chapter 12. Microkernel Architecture Style
Also called the plug-in architecture. Decades old and still common. A natural fit for product-based applications (packaged, downloadable, installed at the customer site as a single monolithic deployment), but also widely used for custom business applications.
Topology
A relatively simple monolith with two component types: a core system and plug-in components. Logic is divided between independent plug-ins and the basic core, giving extensibility, adaptability, and isolation of features and custom logic.
Core system
Formally, the minimum functionality required to run the system — e.g. the Eclipse core is just a basic text editor until plug-ins make it useful. Alternatively, the core is the “happy path” general processing flow with little or no custom logic. Pulling the cyclomatic complexity out of the core into plug-ins improves extensibility, maintainability, and testability.
Example: an electronics-recycling app that must assess each device with device-specific rules. Rather than a giant if/else chain in the core (assessiPhone6s(), assessiPad1(), assessGalaxy5()…), each device becomes its own plug-in. The core then just looks up and invokes the right plug-in from a registry — adding a new device is a matter of adding a plug-in and a registry entry:
public void assessDevice(String deviceID) {
String plugin = pluginRegistry.get(deviceID);
Class<?> theClass = Class.forName(plugin);
Constructor<?> constructor = theClass.getConstructor();
DevicePlugin devicePlugin = (DevicePlugin)constructor.newInstance();
devicePlugin.assess();
}The core can itself be a layered architecture or modular monolith, and can even be split into separately deployed domain services (e.g. a Payment Processing core with credit-card/PayPal/gift-card plug-ins). It’s typical for the whole monolith to share one database. The presentation layer can be embedded in the core or be a separate UI (which can itself be a microkernel).
Plug-in components
Standalone, independent components holding specialised processing, extra features, and custom/volatile code. They should be independent of each other with no inter-plug-in dependencies.
- Communication is generally point-to-point — a method/function call into the plug-in’s entry-point class.
- Plug-ins can be compile-based (simpler to manage, but require redeploying the whole monolith on change) or runtime-based (added/removed at runtime without redeploying, managed via frameworks like OSGi, Penrose, Jigsaw (Java), or Prism (.NET)).
- Implemented as shared libraries (JAR, DLL, Gem) or as namespaces/packages in the same code base. Recommended namespace convention:
app.plugin.<domain>.<context>(e.g.app.plugin.assessment.iphone6s) — making it clear it’s a plug-in, grouping by domain, and pinpointing the specific context.
Plug-ins don’t have to be point-to-point — they can be invoked over REST or messaging, each plug-in being a standalone service or even a microservice. But note: even so, this is still a single architecture quantum, because every request must first go through the core system.
Despite each plugin technically being independently deployable, they’re still a single architecture quantum because requests must first go through the core system.
I’m not sure I get this. Why does every request need to go through the core? If plugins expose a REST API, surely something other than the core could call them? Might make more sense once I see a real system built this way.
Remote plug-in access brings benefits (better decoupling, scalability/throughput, runtime changes without special frameworks, and asynchronous invocation that can improve responsiveness — e.g. kicking off an assessment async and being notified on completion). But the trade-offs: it turns the microkernel into a distributed architecture (hard to ship as a third-party on-prem product), adds complexity and cost, complicates deployment, and means an unresponsive plug-in (especially over REST) can block a request — which wouldn’t happen in a monolithic deployment.
Plug-ins generally don’t connect directly to a shared database — the core owns that and passes data in, so a DB change impacts only the core (decoupling). Plug-ins may have their own private data store (external, embedded, or in-memory), e.g. each device-assessment plug-in holding its own rules.
Registry
The core needs to know which plug-ins exist and how to reach them, via a registry holding each plug-in’s name, data contract, and access details. It can be as simple as an in-memory map (key → plug-in reference) or as complex as a discovery tool (Apache ZooKeeper, Consul). A simple Java registry can hold point-to-point, messaging, and RESTful entries side by side:
registry.put("iPhone6s", "Iphone6sPlugin"); // point-to-point
registry.put("iPhone6s", "iphone6s.queue"); // messaging
registry.put("iPhone6s", "https://atlas:443/assess/iphone6s"); // restfulContracts
Contracts between plug-ins and the core are usually standard across a domain (behaviour, input, output). Third-party plug-ins may impose custom contracts — handle these with an adapter so the core needn’t carry special code per plug-in. Contracts can be XML, JSON, or passed objects. Example: a Java AssessmentPlugin interface defining assess(), register(), deregister() and returning an AssessmentOutput (report string, resell flag, value, resell price). Note the roles/responsibility split — the core doesn’t understand the report’s details, only displays or prints it.
Examples and use cases
- Product software: Eclipse, PMD, Jira, Jenkins; web browsers like Chrome/Firefox (plug-ins/extensions add capabilities to a basic core).
- Insurance claims processing: jurisdiction-specific rules (one state allows free windshield replacement, another doesn’t) create an almost infinite set of conditions, often a tangled rules engine. Put each jurisdiction’s rules in a separate plug-in so they can change independently; the core is the standard, rarely-changing claim process.
- Tax preparation: the US 1040 form is the core (driver); each supporting form/worksheet is a plug-in, so tax-law changes are isolated to independent plug-ins.
Trade-offs
Uniquely, microkernel is the only style that can be both domain- and technically partitioned — most are technically partitioned, but a strong domain-to-architecture isomorphism (per-location/per-client configuration, or strong user customisation/extensibility like Jira or Eclipse) makes domain partitioning natural. Like layered, the number of quanta is always 1, since all requests go through the core.
Characteristic ratings (1 = poorly supported, 5 = defining strength):
| Characteristic | Rating |
|---|---|
| Simplicity | High — primary strength |
| Cost | High (cheap) — primary strength |
| Testability | 3 / above average |
| Deployability | 3 / above average |
| Reliability | 3 / above average |
| Modularity | 3 / above average |
| Evolvability | 3 / above average |
| Performance | 3 / above average |
| Scalability | Low — main weakness |
| Elasticity | Low — main weakness |
| Fault tolerance | Low — main weakness |
| Number of quanta | Always 1 |
Reasoning:
- Simplicity & cost are the strengths; scalability, elasticity, fault tolerance the weaknesses — all due to the typical monolithic deployment.
- Testability, deployability, reliability rate above average because functionality is isolated in independent plug-ins, narrowing test scope and deployment risk (especially with runtime plug-ins).
- Modularity & evolvability rate above average — features can be added/removed/changed via self-contained plug-ins (e.g. add a new tax form as a plug-in when the law changes; remove an obsolete one).
- Performance rates above average mostly because microkernel apps stay relatively small, suffer less from the sinkhole anti-pattern, and can be streamlined by unplugging unneeded functionality (e.g. WildFly/JBoss running faster with clustering/caching/messaging removed).
Source
Mark Richards & Neal Ford, Fundamentals of Software Architecture, Chapter 12.