Archtin
All articles
ArchitectureSystem Design11 min read

Microkernel (Plugin) Architecture: A Small Core and Infinite Variation

A minimal core plus plugins loaded against a stable extension contract. It powers VS Code, browsers, Kubernetes, CI systems and every product that must vary by customer, country or rule set without forking.

The shape

A microkernel architecture has exactly two kinds of component. The core provides the invariant machinery: lifecycle, configuration, a registry of extension points, dispatch, and the shared services plugins are allowed to use. Plugins supply all variable behaviour by registering against those extension points.

The core must know nothing about any specific plugin. The moment the core contains if (plugin === "acme"), the architecture has already failed.

What belongs in the core

In the coreIn plugins
Plugin discovery, load order, lifecycleDomain rules that vary by customer
Extension point registry and dispatchFormat readers/writers, connectors
Shared infra: logging, config, storage accessValidation policies, pricing rules
Security and sandboxingUI panels, commands, themes
Versioning and compatibility checksIntegrations with third-party systems
The sizing test
Ask: "could this be different for one customer, one country, or one deployment?" If yes, it is a plugin. If it must be true for all of them forever, it is core.

Designing the extension contract

The contract is the architecture. Everything else is plumbing. Get it wrong in either direction and you pay for years:

  • Too narrow and plugins reach around it — importing internals, patching globals, scraping the DOM. Now the core cannot change without breaking them.
  • Too broad and you have exposed your internals as public API. Every refactor is a breaking change for the ecosystem.
// core defines the extension points
interface PricingPlugin {
  id: string;
  apiVersion: 1;                       // compatibility gate
  supports(ctx: OrderContext): boolean;
  price(ctx: OrderContext): Promise<PriceBreakdown>;
}

// core dispatch — knows no plugin by name
const plugin = registry.pricing.find(p => p.supports(ctx))
             ?? registry.pricing.default;
const breakdown = await withTimeout(plugin.price(ctx), 200);

// plugin registers itself
register("pricing", { id: "eu-vat", apiVersion: 1, supports, price });
A hook-based contract: the core asks, plugins answer.

Three details make this durable: an explicit apiVersion so incompatible plugins are rejected at load rather than crashing at runtime, a supports() predicate so selection is data-driven, and a timeout so a slow plugin cannot hang the core.

Registry, discovery and lifecycle

  1. Discover: scan a directory, read a manifest, or fetch from a marketplace. Manifests declare id, version, required core version, permissions and extension points used.
  2. Resolve: check compatibility and dependencies between plugins; fail loudly and skip rather than half-loading.
  3. Activate: lazily where possible — VS Code's activation events are the canonical example, keeping startup fast with hundreds installed.
  4. Dispatch: the core invokes hooks; plugins never invoke each other directly (they communicate through core-mediated events).
  5. Deactivate: plugins must release resources; the core must survive one that does not.

Isolation and trust

A plugin is untrusted code running inside your product. Decide the trust model explicitly:

  • In-process, trusted (first-party only): fastest, zero isolation, one bad plugin crashes everything.
  • Separate process (browser tabs, VS Code extension host): a crash or hang is contained; costs IPC and serialisation.
  • Sandboxed runtime (WASM, V8 isolates): capability-scoped, memory-limited, language-agnostic. The modern default for third-party plugins.

Whatever you pick, enforce per-plugin timeouts, resource caps and a permission model — a plugin should have to declare that it needs network or filesystem access.

Real systems

SystemCorePlugins
VS CodeEditor, UI shell, extension hostLanguage servers, themes, debuggers
BrowsersRendering, JS engine, sandboxExtensions with declared permissions
KubernetesAPI server, scheduler, control loopCRDs + controllers, CNI, CSI drivers
Jenkins / CIJob scheduling and workspaceBuild steps, SCM, publishers
Payment platformsLedger, orchestration, idempotencyPer-gateway and per-country adapters
Rule enginesEvaluation loopRule packs per market

Trade-offs

  • Wins: variation without forking; third parties extend without core access; features ship on independent timelines; the core stays small and testable.
  • Costs: contract design is genuinely hard; debugging spans core and plugins; versioning an ecosystem is a permanent commitment; dispatch adds indirection; testing combinations grows quickly.
  • Wrong when: variation is minor and known — three if-branches beat a plugin framework every time.

Interview framing

Strong answer

"Tax rules differ per country and change without our release cycle, so I'd make the core an orchestration engine with a TaxRulePlugin extension point: a manifest with an API version, a supports(jurisdiction) predicate and a pure calculate() hook with a hard timeout. Plugins load in a sandboxed runtime with declared permissions so a bad third-party rule pack cannot take down checkout. The core never names a plugin; selection is data-driven from the registry. The main risk is contract design, so I'd keep the first version minimal and version it explicitly."

Follow-ups

  • How do you evolve the contract without breaking existing plugins?
  • What happens when two plugins claim the same extension point?
  • How do you stop a plugin from degrading core latency?

Keep reading

Suggested next articles based on this one.

Design it, don't just read it.

Practise LLD and system design problems with structured rubrics and AI feedback.

Start practising free