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 core | In plugins |
|---|---|
| Plugin discovery, load order, lifecycle | Domain rules that vary by customer |
| Extension point registry and dispatch | Format readers/writers, connectors |
| Shared infra: logging, config, storage access | Validation policies, pricing rules |
| Security and sandboxing | UI panels, commands, themes |
| Versioning and compatibility checks | Integrations with third-party systems |
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 });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
- Discover: scan a directory, read a manifest, or fetch from a marketplace. Manifests declare id, version, required core version, permissions and extension points used.
- Resolve: check compatibility and dependencies between plugins; fail loudly and skip rather than half-loading.
- Activate: lazily where possible — VS Code's activation events are the canonical example, keeping startup fast with hundreds installed.
- Dispatch: the core invokes hooks; plugins never invoke each other directly (they communicate through core-mediated events).
- 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
| System | Core | Plugins |
|---|---|---|
| VS Code | Editor, UI shell, extension host | Language servers, themes, debuggers |
| Browsers | Rendering, JS engine, sandbox | Extensions with declared permissions |
| Kubernetes | API server, scheduler, control loop | CRDs + controllers, CNI, CSI drivers |
| Jenkins / CI | Job scheduling and workspace | Build steps, SCM, publishers |
| Payment platforms | Ledger, orchestration, idempotency | Per-gateway and per-country adapters |
| Rule engines | Evaluation loop | Rule 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?