Plugin / Microkernel Architecture
What a plug-in (microkernel) architecture is, the design decisions behind contracts, loading, isolation and versioning, and how to apply it to a configuration tool with many device families. For architects designing or reviewing extensible tools.
- Researched
- 15 Oct 2026
- Reading time
- 7 min
- Sources
- 25
- Review due
- 15 Oct 2027
- Small core, independent plug-ins: the core holds only what is needed to run; plug-ins add specialised behaviour behind contracts.
- The contract is the product: design and version it first; every core change becomes an API negotiation.
- Plug-ins declare data, the core renders it: parameter definitions, rules and UI contributions as data keep plug-ins small and safe.
- Isolation is a spectrum: separate load contexts handle dependency conflicts; untrusted or crash-prone plug-ins need a separate process.
- Configuration tools are a classic fit: EB tresos Studio delivers device support as Eclipse plug-ins.
On this page 10 sections
The idea in one minute
A microkernel or plug-in architecture has two kinds of component: a core system that contains only the minimal functionality needed to run, and plug-in modules that add specialised behaviour. Plug-ins must stay independent of each other. The core finds them through a registry and talks to them through contracts, and Mark Richards advises deciding a contract versioning strategy at the start.[1]
The name comes from operating systems, where microkernels such as Mach, L4, QNX and MINIX 3 run drivers and file systems outside the kernel so a crashed service can be restarted without a reboot.[24] In application design the same shape appears as Fowler’s Plugin pattern: implementations linked at run time through configuration rather than at compile time.[25]
When it fits
| Fits when… | Doesn’t fit when… |
|---|---|
| Variability sits on a stable, well-understood axis: device families, protocols, regional rules, file formats | The variability is not understood yet, so a contract cannot be designed |
| New variants must ship without a core release, or partners and OEMs add them | One team, no third parties, everything ships together (a modular monolith is cheaper) |
| The product is long-lived and its core changes slowly | Scaling, not extensibility, drives the design |
| You can afford to treat the contract as a published API | Plug-ins would constantly need each other |
Richards rates the style high on agility, deployment, testability and performance, and low on scalability and ease of development.[1]
The design decisions
Contract and versioning
- Separate contracts package. Microsoft’s plugin tutorial puts the interface in its own library so it can be published as a package.[2]
- Semantic version ranges. OSGi distinguishes consumers from providers: a provider of an API should import it with a narrower range than a consumer.[18]
- API stability markers and verification. IntelliJ marks internal, experimental and scheduled-for-removal APIs and ships a Plugin Verifier that checks binary compatibility across IDE versions.[16]
- Compatibility declared up front. VS Code requires an
enginesfield;[9] IntelliJ uses since and until build ranges.[15]
Discovery
Plug-ins describe themselves in a manifest that the core reads before loading any code. Eclipse plug-ins declare contributions to named extension points;[17] IntelliJ does the same in plugin.xml;[13] VS Code uses package.json with contribution points and activation events;[9] .NET’s MEF scans a directory and matches parts to imports by contract.[5]
Loading and isolation
- .NET: one
AssemblyLoadContextper plug-in, with a dependency resolver that reads the plug-in’s own dependency file, so plug-ins can carry conflicting versions of libraries.[2] - Share the contract, or types will not match. The contracts assembly must load once, from the default context; otherwise the same type loaded twice is two different types.[3]
- A load context is not a security boundary. Untrusted code cannot be safely loaded into a trusted .NET process.[2]
- Unloading is cooperative. A collectible context stays alive while anything still references it.[4] IntelliJ’s dynamic plugins have the same constraint: one leaked reference forces a restart.[14]
- Process isolation for anything risky. VS Code moved its extension host into a separate process so misbehaving extensions do not affect the editor.[6][7]
Lifecycle and activation
OSGi bundles move through installed, resolved, starting, active and stopping states, and can activate lazily on first use.[19] VS Code activates extensions on events (a command, a file type, a workspace pattern) and discourages activating on startup.[8] Lazy activation keeps start-up fast when dozens of plug-ins are installed.
Trust
VS Code states that extensions run with the same permissions as the editor itself, and relies on marketplace scanning, signing and publisher trust.[11] In engineering tools, signed plug-in manifests serve the same purpose: EB tresos plug-ins ship signature manifests in META-INF.[23]
Data, not code, for UI
VS Code’s settings contributions are declared as schema-like data that the editor renders without running extension code.[10] For a configuration tool this is the model to copy: plug-ins declare parameter definitions; the core renders the editor.
Worked example: a configuration tool
An illustrative C# sketch for device families delivered as plug-ins.
// Contracts package: shared, loaded once from the default context
public interface IDeviceFamilyPlugin
{
string FamilyId { get; } // "acme.ecu.bms"
IReadOnlyList<ParameterDefinition> GetParameters(DeviceVariant variant);
IEnumerable<ValidationIssue> Validate(IConfigurationSnapshot config);
IDeviceTransport CreateTransport(TransportOptions options); // a port, as in hexagonal
}
public sealed record ParameterDefinition(
string Key, string DisplayName, ParamType Type,
object? Default, object? Min, object? Max, IReadOnlyList<string>? Choices, string Group);
// Host: one load context per plug-in, contracts forced to the shared copy
sealed class PluginLoadContext(string pluginPath) : AssemblyLoadContext(isCollectible: true)
{
private readonly AssemblyDependencyResolver _resolver = new(pluginPath);
protected override Assembly? Load(AssemblyName name) =>
name.Name == "Acme.Configurator.Contracts" ? null // use the host's contracts
: _resolver.ResolveAssemblyToPath(name) is { } path ? LoadFromAssemblyPath(path)
: null;
}
{
"id": "acme.ecu.bms",
"version": "2.3.0",
"entryAssembly": "Acme.Bms.Plugin.dll",
"contract": ">=3.1 <4.0",
"families": ["BMS-G2", "BMS-G3"],
"activation": ["onDeviceDetected:0x1A2B", "onProjectContains:*.bmscfg"]
}
The core reads the manifest, rejects incompatible contract ranges before loading anything, and activates the plug-in only when a matching device or project appears. A plug-in that wraps a native vendor driver runs in a helper process and talks to the core over IPC instead.
Where it is used in practice
- EB tresos Studio (AUTOSAR ECU configuration) is built on Eclipse and extended with plug-ins.[22] Texas Instruments ships its microcontroller drivers’ configuration as tresos plug-ins: parameter definitions, presets, code-generation templates, signature manifests and a
plugin.xml, copied into the tool’s plugins folder.[23] This is the same pattern applied to exactly the kind of tool this article is about. - Eclipse describes plug-ins as structured bundles that contribute functionality at well-defined extension points.[17]
- VS Code combines a separate extension host process, activation events and signed marketplace packages.[6][11]
- IntelliJ Platform offers extension points, dynamic loading and automated compatibility verification.[13][16]
How it relates to other styles
- Hexagonal / Clean. Any device that follows a port’s protocol can be plugged into it;[20] frameworks and drivers are outer-ring details.[21] A plug-in is an adapter that ships separately and loads at run time. The difference: hexagonal adapters usually face infrastructure, while plug-ins usually carry domain variability, so their contract sits at the domain boundary and must be designed as a public API.
- Modular monolith. Both are one deployable with internal parts. Modules are compiled in and versioned together; plug-ins are discovered at run time, versioned independently and may come from third parties.
Gains and costs
What it buys
- Ship variants without a core release. New device families arrive as plug-ins.
- Isolated variability. Family-specific code cannot leak into the core.
- Ecosystem. Partners, OEMs and customers can extend the tool.
- Testable in isolation. Each plug-in can be tested against the contract alone.
- Lean start-up. Lazy activation loads only what a project needs.
What it costs
- Contract churn. Every core change becomes a breaking-change discussion.
- Dependency conflicts between plug-ins and type-identity traps across load contexts.
- Isolation is costly. In-process is fast but fragile; out-of-process is safe but needs IPC.
- Security. Plug-ins run with the host's rights unless you isolate them.
- Harder to develop. Debugging across the core and many plug-ins is slower.
How it goes wrong
- Contract designed too early. The variability axis was not understood, so the contract changes with every new family.
- Plug-ins depending on plug-ins. Independence breaks and load order starts to matter.
- The core as a god object. Everything that does not fit a contract lands in the core, which becomes a service locator for everything.
- In-process vendor code. A crash or leak in one native driver takes down the whole tool or blocks unloading.[4]
- Eager activation. Every plug-in starts at launch and start-up time grows with each family.[8]
- No compatibility checks. Old plug-ins load against a new core and fail at run time instead of being rejected up front.
Testing a plug-in system
| Level | What | How |
|---|---|---|
| Contract test kit | Every plug-in honours the contract | Abstract tests shipped with the contracts package: unique parameter keys, defaults within ranges, deterministic validation, clean disposal |
| Host | Loading, activation, rejection, unloading | A fake plug-in; check that an unloaded context is actually collected[4] |
| Compatibility matrix | Each plug-in version against each supported core and contract version | Prior art: the IntelliJ Plugin Verifier across IDE versions[16] and VS Code tests against pinned editor versions[12] |
| Plug-in internal | Rules and parameter logic | Ordinary unit tests; decoupled parts make a harness easy[5] |
Origins
- Operating systems: microkernels moved drivers and services out of the kernel; the architectural style borrows the name and the idea.[24]
- 2002: Fowler documents the Plugin pattern in Patterns of Enterprise Application Architecture.[25]
- 2000s: Eclipse builds a whole IDE platform on plug-ins and OSGi bundles, later the basis of engineering tools such as EB tresos Studio.[17][22]
- 2015: Richards describes the microkernel as one of five core architecture patterns.[1]
- 2022: VS Code completes the move of extensions into a separate process for stability.[7]
Key takeaways
- Use it when variability is stable and well understood (device families, protocols, regional rules) and must ship without a core release.
- Put the contract in its own versioned package, declare compatible ranges in a manifest, and check compatibility before loading code.
- Keep plug-ins independent of each other; if they need each other, they belong in the core or in one plug-in.
- A load context is not a security boundary; vendor DLLs and third-party code go out of process.
- Ship a contract test kit with the contracts package and run a compatibility matrix in CI.
Sources
- Software Architecture Patterns: microkernel architecture · Mark Richards, 2015Primaryoreilly.com
- Create a .NET Core application with plugins · Microsoft LearnPrimarylearn.microsoft.com
- About AssemblyLoadContext · Microsoft LearnPrimarylearn.microsoft.com
- How to use and debug assembly unloadability · Microsoft LearnPrimarylearn.microsoft.com
- Managed Extensibility Framework (MEF) · Microsoft LearnPrimarylearn.microsoft.com
- Extension Host · VS Code documentationPrimarycode.visualstudio.com
- Migrating VS Code to process sandboxing · VS Code blog, 2022Primarycode.visualstudio.com
- Activation events · VS Code documentationPrimarycode.visualstudio.com
- Extension manifest · VS Code documentationPrimarycode.visualstudio.com
- Contribution points · VS Code documentationPrimarycode.visualstudio.com
- Extension runtime security · VS Code documentationPrimarycode.visualstudio.com
- Testing extensions · VS Code documentationPrimarycode.visualstudio.com
- Extension points · IntelliJ Platform SDKPrimaryplugins.jetbrains.com
- Dynamic plugins · IntelliJ Platform SDKPrimaryplugins.jetbrains.com
- Build number ranges · IntelliJ Platform SDKPrimaryplugins.jetbrains.com
- Verifying plugin compatibility · IntelliJ Platform SDKPrimaryplugins.jetbrains.com
- Eclipse platform architecture · Eclipse documentationPrimaryhelp.eclipse.org
- OSGi Core 8: module layerPrimarydocs.osgi.org
- OSGi Core 8: life cycle layerPrimarydocs.osgi.org
- Hexagonal Architecture · Alistair Cockburn, 2005Primaryalistair.cockburn.us
- The Clean Architecture · Robert C. Martin, 2012Primaryblog.cleancoder.com
- EB tresos Studio · Elektrobit (vendor)Primaryelektrobit.com
- MCAL configurator and EB tresos · Texas Instruments (vendor)Primarysoftware-dl.ti.com
- Microkernel · WikipediaReferenceen.wikipedia.org
- Plugin · Martin Fowler, Patterns of Enterprise Application ArchitectureReferencemartinfowler.com
Referenced by
- Which Architecture Style When? Comparison