Leer en español · Lire en français · Auf Deutsch lesen
Section 5.5 of 5.8 · Intermediate · 24 min read
Microkernel and plug-in architectures
Video lesson
4 min · narrated, with captionsListen only (audio)
Read the transcript
Many successful products are a small core that others extend. In this lesson you'll learn how the microkernel, or plug-in, style works, where it fits, and how to keep its contracts healthy over years.
Think of a games console. The console itself does a few general things: it starts up, shows a menu, reads the controllers and draws pictures. Each game is a cartridge that slots in through a standard connector. You can add new games for years without opening the console, and a badly made game can't rewire it. A microkernel architecture builds software the same way.
Here's the shape. The core system holds the minimal, general logic: the main workflow and the points where plug-ins attach. In a food-delivery checkout, that's pricing a basket, taking payment and confirming the order. Inside the core, a registry records which plug-ins exist, their versions and the contract each implements. It lets the core ask for the tax rules for Germany without knowing which class provides them. Plug-ins connect only through contracts the core publishes. On one side, tax rules for the UK, Germany and the United States. On the other, payment methods: cards, wallets and cash on delivery. Two rules hold it together. Plug-ins never call each other, and the core never names a particular plug-in.
The style suits products with a stable workflow but many variants of particular steps. Tax and payroll rules by country, insurance rules by jurisdiction, payment methods, file importers. It also suits platforms that want an ecosystem of third-party extensions. Development tools such as Eclipse and Visual Studio Code, and web browsers, are built this way. Visual Studio Code even runs extensions in a separate process, so a misbehaving one can't freeze the editor.
Let's design one. The food-delivery platform is launching in Germany and the United States after the UK, and its checkout is already full of branches for each country. First, name the variation point. Only the tax on a priced basket varies by country. Pricing, payment and receipts stay in the core, which keeps the contract small. Next, decide what crosses the contract. Basket lines go in, each with a neutral category such as hot food or delivery fee. Total tax and a labelled breakdown come out. No orders, customers or database sessions are passed, so plug-ins can't depend on core internals. The contract becomes a small, separately versioned package. Each plug-in declares the version it needs, and the registry checks at start-up, so a mismatch shows up in deployment, not as wrong tax on a receipt. If no plug-in exists for a country, checkout refuses the order rather than charging zero tax. Finally, one shared contract test suite runs against every plug-in. Adding a fourth country is now a new package and its tests, not a change to checkout.
Here it is at run time. Checkout asks the registry for the plug-in registered for Germany, and gets it back. Then it calls that plug-in through the contract. Notice the United States plug-in. It was built for an older major version of the contract, so the registry skipped it at start-up, with a log message, instead of crashing checkout.
Three things keep a plug-in architecture healthy. First, isolation. In-process plug-ins are fast, but one with a memory leak affects everything. Your own team's tax rules can run in-process; arbitrary code from a public marketplace should not. Second, versioning. Prefer additive changes, such as new optional fields, so old plug-ins keep working. Third, Hyrum's law: with enough users, every observable behaviour of an interface will be depended on by someone. So keep the contract small, and treat anything plug-ins can see as part of it. And don't build extension points for imagined needs. Wait until you've built the same kind of variation two or three times, then extract the contract from the real cases.
The full section explains how this differs from an operating-system microkernel, lists the common anti-patterns, and shows the contract and registry in code. Next, in Section five point six: hexagonal, onion and clean architecture. See you there.
What you need to know
- A microkernel (plug-in) architecture has a minimal core system plus independent plug-ins that connect only through published contracts, found through a registry.
- It suits products with a stable workflow and many variants of particular steps, such as tax rules by country, payment methods, file importers or editor languages.
- Plug-ins should depend only on the contract, never on each other or on core internals, and the core should never contain special cases for named plug-ins.
- Isolation is a trade-off: in-process plug-ins are fast but share the host's fate; separate processes, sandboxes or remote calls protect the core at a cost.
- Version the contract separately, check compatibility when plug-ins load, and prefer additive changes; Hyrum's law means anything plug-ins can observe becomes part of the contract.
- Application microkernels aim at extensibility; operating-system microkernels such as L4 or QNX aim at isolation and a small trusted kernel.
Definition and origin
A microkernel architecture, also called a plug-in architecture, splits an application into two kinds of part: a core system that contains the minimal, general functionality needed to run, and plug-ins that add or change behaviour without the core being modified. Plug-ins connect to the core only through published contracts, and the core finds them through a registry. Mark Richards and Neal Ford catalogue it as one of the classic architecture styles in Fundamentals of Software Architecture, and it fits the description of an architectural style given in Section 5.1: a named set of constraints on the elements (core, plug-ins) and connectors (contracts) of a system.
The idea grew up in product software: tools that are installed by many customers, each of whom wants something slightly different. Rather than maintain a separate version for each customer, the vendor ships one core and lets customers, partners or the vendor's own teams add plug-ins. Integrated development environments, web browsers, graphics editors and accounting packages all work this way. The name is borrowed from operating systems, where a microkernel is a very small kernel with everything else running outside it; the two ideas are related but should not be confused, as explained below.
A microkernel application is normally a single deployment unit, so it belongs with the monolithic styles of this chapter: one deployment unit, one process, one set of operational concerns. Plug-ins may be installed separately, but at run time they live inside, or right next to, the core.
The shape of a microkernel system
How it works: core, plug-ins, registry and contracts
The core system contains what every installation needs: the main workflow, shared services such as logging or configuration, and the extension points where plug-ins attach. Richards and Ford describe it as the happy path through the application with little or no custom processing; in a food-delivery checkout, that is pricing a basket, taking payment and confirming the order. A good core is deliberately boring. Every special case that varies by customer, country or product line is a candidate to move out of it.
Plug-ins are independent, self-contained modules that contain specialised processing. They should depend only on the contract, not on each other and not on the core's internal classes. When one plug-in needs something another provides, the need should be met by the core (as a service it exposes through the contract) or the two plug-ins should be merged; plug-in-to-plug-in dependencies quietly turn a clean star into a tangle.
The registry records which plug-ins are available, what each is called, which version it is, which contract it implements and how to reach it. It can be as simple as a dictionary filled at start-up, a directory the core scans for manifests, entries in a package's metadata (as with Python entry points), a database table, or a remote service that lists plug-ins deployed elsewhere. The registry is what lets the core say "give me the tax rules for Germany" without knowing that a class called GermanTax exists.
Contracts define what a plug-in must provide and what it may rely on: an interface with method signatures, the data structures that cross it, the error behaviour and a version. In Eclipse a contract is an extension point declared in a plug-in manifest with a schema; in Visual Studio Code it is the extension API plus the contribution points listed in an extension's package.json; in your own code it is usually an interface (or a Python Protocol) in a small package of its own. Where the plug-in comes from another organisation and cannot follow your contract, an adapter translates between the two, exactly as in hexagonal architecture.
Plug-ins can be connected in three ways. Point-to-point, in-process calls are the norm: the core calls a method on the plug-in object. Separate-process plug-ins run in their own process and talk to the core through inter-process messages, which protects the core from crashes. Remote plug-ins sit behind an HTTP or messaging interface; this allows plug-ins in other languages or owned by other teams, but it adds network latency and failure modes, and the system starts to look like the distributed styles of Chapter 6.
Real examples
Eclipse. The Eclipse IDE is the textbook case. Its core is little more than a workbench and a plug-in runtime (built on Equinox, an implementation of the OSGi framework specification). Almost everything a developer sees, from the Java tooling to version-control integration, is a plug-in. Each plug-in has a manifest that declares what it needs, which extension points it offers to others, and which extensions it plugs into. Plug-ins are activated lazily, only when something actually needs them, so that hundreds of installed plug-ins do not slow start-up.
Visual Studio Code. Extensions declare activation events (open a Markdown file, run a command) so they load only when needed, and contribute commands, languages, themes and views through their manifest. Extensions run in a separate extension host process, so a misbehaving extension cannot freeze the editor's user interface. This is a good example of choosing process isolation for a plug-in ecosystem that includes thousands of third-party authors.
Web browsers. Browser extensions use a standard set of extension APIs and must declare the permissions they need (which sites they may read, whether they may see tabs or downloads) in a manifest. The browser enforces those permissions, because extension authors cannot be trusted by default.
Business rules engines. An insurer's claims system may keep the claims workflow in the core and put each jurisdiction's rules (what is covered, which documents are needed, statutory deadlines) in plug-ins. Tax engines, payroll systems and e-commerce platforms with payment-method plug-ins follow the same pattern: a stable process, with rules or providers that vary by country, product or customer.
Developer tools. The pytest test framework is built around the pluggy plug-in library; many of its own features are implemented as plug-ins using the same hooks that third parties use. Build tools, linters and static-site generators commonly do the same.
Application microkernel versus operating-system microkernel
| Aspect | Operating-system microkernel | Application microkernel (plug-in) architecture |
|---|---|---|
| What is minimal | The kernel: address spaces, threads, scheduling and inter-process communication | The core system: the general workflow of one application |
| What is outside the core | Device drivers, file systems and network stacks, run as separate user-space servers | Variable business behaviour or features, such as tax rules, payment methods or editor languages |
| Main goals | Reliability, security and a small trusted computing base | Extensibility, customisation and a third-party ecosystem |
| How parts talk | Message passing between isolated processes | Usually in-process calls through an interface; sometimes a separate process or remote call |
| Examples | Mach, the L4 family (including the formally verified seL4), QNX, MINIX 3 | Eclipse, Visual Studio Code, web browsers, pytest, insurance and tax rules engines |
Plug-in isolation, versioning and compatibility
Three questions decide whether a plug-in architecture stays healthy over years: how isolated plug-ins are, how the contract evolves, and how incompatibility is detected.
Isolation. In-process plug-ins share the core's memory, threads and permissions. That is fast and simple, but one plug-in with an infinite loop, a memory leak or a security hole affects everything. The options, in increasing order of isolation and cost, are: in-process with defensive calls (timeouts, catching exceptions per plug-in, resource limits); a separate process or worker per plug-in or group of plug-ins; a sandbox such as a WebAssembly runtime or a restricted interpreter, which limits what plug-in code can touch; and a remote service. Choose by asking who writes the plug-ins. Your own team's tax rules can run in-process; arbitrary code from a public marketplace should not.
Versioning. The contract is an API, and the rules of Section 0.6 about breaking changes apply. Version the contract separately from the core, usually with semantic versioning: a new major version for breaking changes, a minor version for backward-compatible additions. Let each plug-in declare which contract version it was built for, and let the core declare which versions it supports. Additive change (new optional fields, new optional hooks) keeps old plug-ins working; when a breaking change is unavoidable, support the old and new major versions side by side for a published deprecation period.
Compatibility checks. Check compatibility when plug-ins are loaded, not when a customer first triggers them. A registry that refuses an incompatible plug-in at start-up, with a clear log message, turns a run-time bug into a deployment failure. Hyrum Wright's observation, known as Hyrum's law, is worth remembering here: with enough users, every observable behaviour of an interface will be depended on by someone, whatever the documentation says. Keep the contract small, keep core internals out of reach (Eclipse marks non-API packages as internal for this reason), and treat anything visible to plug-ins as part of the contract.
Characteristics of the microkernel style
| Characteristic | Rating | Reason |
|---|---|---|
| Extensibility and modifiability (in the planned directions) | High | New behaviour arrives as a new plug-in; the core and the other plug-ins do not change. |
| Simplicity | Medium | The idea is simple, but contracts, a registry, loading and version checks are real machinery. |
| Testability | Medium to high | Each plug-in can be tested alone against the contract; the core can be tested with fake plug-ins. Combinations still need testing. |
| Deployability | Medium | Plug-ins can often be added without redeploying the core; a contract change forces coordinated releases. |
| Performance | Medium to high | In-process plug-ins are ordinary function calls; isolation by process or sandbox adds overhead. |
| Scalability and elasticity | Low | It is usually one deployment unit, so it scales as a whole, like any monolith. |
| Fault tolerance | Low to medium | An in-process plug-in can crash or slow the host; process isolation improves this at a cost. |
| Overall cost | High (cheap) | One codebase and one runtime to operate; the main cost is designing and evolving contracts. |
When to use it, and when not
Good fits. Product software that many customers install and customise. Systems with a stable workflow but many variants of one step: tax and payroll by country, insurance rules by jurisdiction, payment methods, file-format importers, shipping carriers. Platforms that want a third-party ecosystem. Systems where a regulator or customer changes rules often and you want those changes to be small, separately tested releases. The style also works inside a larger system: one service in a distributed architecture can itself be a microkernel.
Poor fits. Systems where you do not yet know what varies. Extension points designed in advance for imagined needs are speculative generality: they add machinery, slow the team down and are usually in the wrong place when the real need arrives (see the discussion of reversibility in Section 1.6). Systems that need very high scalability or fault isolation, since the whole thing scales and fails as one unit. Systems where plug-ins would need to coordinate closely with each other, which defeats the style's main benefit.
A practical rule: wait until you have built the same kind of variation two or three times, then extract the contract from the real cases. A contract derived from three working tax implementations will be far better than one guessed before the first.
Worked example
Worked example: designing a plug-in contract for country-specific tax rules
The food-delivery platform used throughout this course is launching in Germany and the United States after the United Kingdom. Each country taxes food, alcohol and delivery fees differently, and US sales tax also varies by state and city. The checkout code already contains if country == "GB" branches. The architect proposes a microkernel: checkout is the core, and each country's rules are a plug-in. These are the decisions, in the order they were made.
| 1. Name the variation point | Only the calculation of tax on a priced basket varies by country. Pricing, discounts, payment and receipts stay in the core. A narrow variation point keeps the contract small. |
|---|---|
| 2. Decide what crosses the contract | Inputs: basket lines with a neutral category (hot-food, alcohol, delivery-fee), net amounts as Decimal and an optional region. Output: total tax and a labelled breakdown for the receipt. No core objects (orders, customers, database sessions) are passed, so plug-ins cannot depend on core internals. |
| 3. Make the contract a separate package | tax_contract contains only data classes and the TaxRules protocol, versioned with semantic versioning. Core and plug-ins both depend on it; neither depends on the other. |
| 4. Define compatibility | A plug-in declares the contract version it needs, such as (2, 1). The core accepts it if the major version matches and the core's minor version is at least as high. Adding an optional field is a minor change; renaming or removing one is a major change. |
| 5. Choose discovery | Plug-ins are ordinary Python packages that register under the entry-point group fooddelivery.tax_rules. Installing a package is enough; no core code lists the countries. |
| 6. Decide failure behaviour | If no plug-in is registered for a country, checkout refuses the order with a clear error rather than charging zero tax. A plug-in that raises an exception fails that checkout only, and the error is reported with the plug-in's name and version. |
| 7. Decide isolation | Plug-ins are written by the company's own country teams and are trusted, so they run in-process for speed. If outside tax vendors were allowed to supply plug-ins, the design would move them behind a remote call with a timeout. |
| 8. Specify the tests | A shared contract test suite (rounding, empty baskets, negative refund lines, unknown categories) runs against every plug-in in its own pipeline. Each country team adds its own cases from published tax guidance. |
The core no longer changes when a country is added, so the launch of a fourth country is a new package plus its tests, not a change to checkout.
The hardest decision is step 2. A category list that is too coarse forces plug-ins to guess; one that is too detailed couples the contract to one country's law. Expect to revise it, and plan the major-version migration when you do.
The registry rejects incompatible plug-ins at start-up, so a version mismatch shows up in a deployment check, not as wrong tax on a customer's receipt.
# package: tax_contract (version 2.1.0)
from dataclasses import dataclass
from decimal import Decimal
from typing import Protocol
CONTRACT_VERSION = (2, 1)
@dataclass(frozen=True)
class TaxLine:
sku: str
category: str # "hot-food", "cold-food", "alcohol", "delivery-fee"
net: Decimal # never float for money
@dataclass(frozen=True)
class TaxResult:
total_tax: Decimal
breakdown: dict[str, Decimal] # label shown on the receipt
class TaxRules(Protocol):
country: str # ISO 3166-1 alpha-2, e.g. "DE"
requires_contract: tuple[int, int]
def calculate(self, lines: list[TaxLine], region: str | None) -> TaxResult: ...from importlib.metadata import entry_points
from tax_contract import CONTRACT_VERSION
class UnsupportedCountry(Exception): pass
class TaxRegistry:
def __init__(self, log):
self._by_country = {}
for ep in entry_points(group="fooddelivery.tax_rules"):
plugin = ep.load()()
major, minor = plugin.requires_contract
if major != CONTRACT_VERSION[0] or minor > CONTRACT_VERSION[1]:
log.warning("skipping %s: needs contract %s", ep.name, plugin.requires_contract)
continue
self._by_country[plugin.country] = plugin
def for_country(self, country):
if country not in self._by_country:
raise UnsupportedCountry(country) # fail loudly, never guess a rate
return self._by_country[country]The tax plug-ins at run time
Enforcing the style in code
A plug-in architecture only stays one if its rules are checked automatically. The structure that works in most languages has three kinds of package: a small contract package with interfaces and data types only; the core, which depends on the contract; and one package per plug-in, each depending on the contract and nothing else of yours.
Then enforce the dependency rules with tooling of the kind covered in Section 3.4: in Python, import-linter can forbid the core from importing any plug-in package and forbid plug-ins from importing each other or the core; in Java, ArchUnit tests or module systems (JPMS, OSGi) can do the same. Fail the build when a rule is broken.
Add a contract test suite: a set of tests written once against the contract and run against every plug-in in that plug-in's own pipeline. It checks behaviour the contract promises (rounding, empty input, error types), not each country's tax law, which the plug-in's own tests cover. Finally, add a start-up check or a deployment smoke test that loads every installed plug-in and verifies its contract version, so that incompatibilities are caught before users find them.
Questions people ask
Is a microkernel just the strategy pattern?
At code level the strategy pattern is the same mechanism: choose an implementation of an interface at run time. The microkernel is the architectural version: plug-ins are separately built, versioned and often separately installed, a registry discovers them, and the contract is a published API with compatibility rules.
Can plug-ins have their own data?
Yes, and often they should, but treat it like the module-owned data of a modular monolith: a plug-in's tables or files belong to it, and the core reaches them only through the contract.
Should configuration replace plug-ins?
For small, data-like differences (a rate, a threshold, a label), yes: configuration is cheaper and safer. Plug-ins are for differences that need logic. Many systems use both: a plug-in per country plus configuration for each country's current rates.
Check your understanding
What three things must a plug-in architecture define besides the core itself?
The plug-ins, the contracts they implement, and a registry that lets the core discover and look up plug-ins without naming them in code.
Why should plug-ins not depend on each other?
Because then they can no longer be installed, removed, tested and versioned independently, which is the main benefit of the style; shared needs belong in the core or the contract.
Why does Visual Studio Code run extensions in a separate process?
So that a slow or crashing extension cannot freeze or crash the editor's user interface: it trades some performance and complexity for isolation from thousands of third-party authors.
When is configuration better than a plug-in?
When the difference between variants is data, such as a rate, a limit or a label, rather than logic. Configuration is simpler and safer to change.
Sources and further reading
Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.
Ask: “We sell a field-service app to 40 customers and each wants custom rules for job scheduling. Should we build a plug-in architecture, or is configuration enough?”