Plug-in Architecture (MicroKernel)
Plug-in Architecture (Microkernel)
The Plug-in (Microkernel) architecture keeps a small, stable core that defines extension points, and extends behavior through plug-ins that implement those points. The core handles the minimal set of functionality required to run; plug-ins add domain-specific features without modifying the core. This enables product variability, third-party extensions, and marketplace-style systems where features can be added or removed at runtime or deployment time.
Common examples: IDEs (VS Code extensions), browsers (add-ons), CMS platforms (WordPress plugins), and enterprise applications with customer-specific modules.
Structure
┌─────────────────────────────────────┐
│ Core │
│ - Plugin registry │
│ - Extension point interfaces │
│ - Lifecycle management │
└──────────┬──────────────────────────┘
│ IPlugin contract
┌──────┴──────┐
│ │
┌───▼───┐ ┌────▼────┐
│Plugin A│ │Plugin B │
│(PDF) │ │(CSV) │
└────────┘ └─────────┘
Implementation in .NET
The core defines the extension point interface:
public interface IPlugin
{
string Name { get; }
void Register(IServiceCollection services);
}
The core loads plug-ins from a directory at startup:
public static void LoadPlugins(IServiceCollection services, string pluginDir)
{
foreach (var dll in Directory.EnumerateFiles(pluginDir, "*.dll"))
{
// Use AssemblyLoadContext for isolation (prevents version conflicts)
var context = new PluginLoadContext(dll);
var assembly = context.LoadFromAssemblyPath(dll);
foreach (var type in assembly.GetTypes()
.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract))
{
var plugin = (IPlugin)Activator.CreateInstance(type)!;
plugin.Register(services);
}
}
}
For more structured plug-in discovery, the Managed Extensibility Framework (MEF) provides attribute-based composition:
[Export(typeof(IPlugin))]
public sealed class PdfPlugin : IPlugin
{
public string Name => "PDF Export";
public void Register(IServiceCollection services) =>
services.AddScoped<IReportExporter, PdfReportExporter>();
}
Pitfalls
Plug-in Version Conflicts
What goes wrong: two plug-ins depend on different versions of the same library. Loading both causes MissingMethodException or silent behavioral differences.
Why it happens: all plug-ins share the same process and CLR by default.
Mitigation: use AssemblyLoadContext to isolate each plug-in's dependencies. Each context has its own assembly resolution scope, preventing version conflicts between plug-ins.
Unstable Extension Point Contracts
What goes wrong: the core's IPlugin interface changes, breaking all existing plug-ins.
Why it happens: the core evolves without treating the extension point as a public API.
Mitigation: version extension point interfaces explicitly. Use adapter patterns to support multiple interface versions simultaneously. Treat IPlugin as a public API with the same stability guarantees as a library.
Tradeoffs
| Approach | Strengths | Weaknesses | When to use |
|---|---|---|---|
| Plug-in architecture | Extensible without modifying core, supports third-party extensions | Complex loading, versioning challenges, security surface | Products with customer-specific modules, marketplaces, IDEs |
| Monolith with feature flags | Simpler, no loading complexity | All features in one codebase, harder to isolate | Internal applications, small teams |
| Microservices | Full isolation, independent deployment | Network overhead, distributed system complexity | High-scale, independent team ownership |
Decision rule: use plug-in architecture when you need runtime extensibility by third parties or when different customers need different feature sets from the same core product. For internal applications where all features are known upfront, a monolith with feature flags is simpler.
Questions
Use AssemblyLoadContext to isolate each plug-in's dependencies. Each context has its own assembly resolution scope, so plug-in A's dependency on Newtonsoft.Json 12.x does not conflict with plug-in B's dependency on 13.x. Cost: each context adds memory overhead and prevents type sharing across contexts — objects from one context cannot be cast to types from another.
Treat the extension point interface as a public API with semantic versioning. For breaking changes, introduce a new interface version (IPlugin, IPlugin2) and support both simultaneously via an adapter. Existing plug-ins implement the old interface; new plug-ins implement the new one. The core adapts old plug-ins to the new contract. Cost: adapter proliferation over time — set a deprecation timeline and remove old interfaces after a migration window.
When all features are known upfront and no third-party extensibility is needed. The loading complexity, versioning challenges, and security surface (untrusted code running in-process) are not justified for internal applications. A monolith with feature flags is simpler and safer. Plug-in architecture earns its complexity when customers or third parties need to extend the product without modifying the core.
References
- AssemblyLoadContext (Microsoft Learn) — the .NET API for isolated plug-in loading; prevents version conflicts between plug-ins by giving each its own assembly resolution context.
- Managed Extensibility Framework (Microsoft Learn) — MEF provides attribute-based plug-in discovery and composition for .NET applications; useful for structured extension point registration.
- Microkernel architecture pattern (Software Architecture Patterns, O'Reilly) — Mark Richards' concise treatment of the Microkernel pattern with real-world examples and tradeoffs.