Plug-in (Microkernel) architecture puts stable product behavior in a small core and exposes contracts that plug-ins implement. The core can run without knowing any concrete extension. Features are selected at deployment or discovered at runtime, which fits products that vary by customer or accept third-party extensions.

IDEs and browsers are familiar examples. CMS platforms and enterprise products use the same structure for optional or customer-specific modules.

How the Core Loads Extensions

┌─────────────────────────────────────┐
│              Core                   │
│  - Plugin registry                  │
│  - Extension point interfaces       │
│  - Lifecycle management             │
└──────────┬──────────────────────────┘
           │ IPlugin contract
    ┌──────┴──────┐
    │             │
┌───▼───┐   ┌────▼────┐
│Plugin A│   │Plugin B │
│(PDF)   │   │(CSV)    │
└────────┘   └─────────┘

Implementation in .NET

The core begins with a narrow extension contract:

public interface IPlugin
{
    string Name { get; }
    void Register(IServiceCollection services);
}

At startup, the host can discover assemblies from a configured directory:

public static void LoadPlugins(IServiceCollection services, string pluginDir)
{
    foreach (var dll in Directory.EnumerateFiles(pluginDir, "*.dll"))
    {
        // Isolates private dependency versions when resolution is configured
        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);
        }
    }
}

The example assumes that PluginLoadContext derives from AssemblyLoadContext and resolves dependencies relative to the plug-in. A separate context per plug-in keeps private dependency versions apart. The host’s extension contract remains shared.

The Managed Extensibility Framework (MEF) is another option when attribute-based discovery and composition fit the host:

[Export(typeof(IPlugin))]
public sealed class PdfPlugin : IPlugin
{
    public string Name => "PDF Export";
    public void Register(IServiceCollection services) =>
        services.AddScoped<IReportExporter, PdfReportExporter>();
}

Unloading and Isolating Plug-ins

  • Unload is cooperative. A new AssemblyLoadContext(name, isCollectible: true) can unload only after every outside reference to its assemblies, types, and instances disappears. An event handler, cached Type, active thread, or static reference can pin the entire context. The AssemblyLoadContext runtime boundary also determines which dependency versions and type identities an extension sees.
  • An assembly-loading boundary is not a security boundary. In-process plug-ins can access the host’s memory, secrets, and filesystem permissions. .NET does not provide an in-process sandbox for untrusted managed code. Untrusted extensions need an OS process, container, or another sandbox with least privilege, with IPC carrying the cost of the real isolation.

Pitfalls

Plug-in Version Conflicts

Two plug-ins may require incompatible versions of the same library. Loading every dependency into the default context can produce type-identity conflicts or failures such as MissingMethodException. Give each plug-in a custom AssemblyLoadContext and resolve its private dependencies there. The shared extension-contract assembly must still come from the host context so both sides agree on the identity of IPlugin.

Unstable Extension Point Contracts

Changing IPlugin can break every extension at once. Treat the contract as a public library API: keep compatible changes compatible, introduce a new contract for breaking behavior, and adapt an older version only while its migration window remains open. Supporting two versions has a real maintenance cost, so deprecation needs an end date.

Tradeoffs

ApproachStrengthsWeaknessesWhen to use
Plug-in architectureExtensible without modifying core, supports third-party extensionsComplex loading, versioning challenges, security surfaceProducts with customer-specific modules, marketplaces, IDEs
Monolith with feature flagsSimpler, no loading complexityAll features in one codebase, harder to isolateInternal applications, small teams
MicroservicesProcess and deployment isolation with explicit network boundariesNetwork overhead, distributed system complexityHigh-scale, independent team ownership

Use plug-in architecture when extensions must ship independently of the core or when customers need different modules from one product. If one team owns every known feature and deploys them together, ordinary modules with feature flags are simpler.

Questions

References