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");       // restful

Contracts

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):

CharacteristicRating
SimplicityHigh — primary strength
CostHigh (cheap) — primary strength
Testability3 / above average
Deployability3 / above average
Reliability3 / above average
Modularity3 / above average
Evolvability3 / above average
Performance3 / above average
ScalabilityLow — main weakness
ElasticityLow — main weakness
Fault toleranceLow — main weakness
Number of quantaAlways 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.