Microkernel (Plugin) Architecture

A small, stable core plus independent plug-ins that add features. How VS Code, browsers, Webpack and ESLint are built, how to design a plug-in contract, and the pitfalls.

Intermediate⏱ 5 min readLesson 10 of 13#architecture#microkernel#plugins#extensibility

The big idea

A smartphone ships with a small, stable operating system. Everything else (maps, games, banking, a flashlight) comes from apps you install. The phone maker doesn't rebuild the OS for each new app; apps plug in through a published contract (the app store rules and APIs).

Microkernel architecture (also called plug-in architecture) works the same way: a minimal core system plus independent plug-in modules that extend it.

Microkernel: a small core with plug-ins attached through a contractMicrokernel: a small core with plug-ins attached through a contract

Where you've already seen it

ProductCorePlug-ins
VS CodeEditor, extension host, APIsLanguage support, themes, Git tools, linters
Web browsersRendering engine, tabs, networkingExtensions (ad blockers, password managers)
Webpack / Vite / RollupModule graph + build pipelineLoaders and plugins (TypeScript, CSS, images)
ESLint / Babel / PostCSSParser + traversal engineRules and transforms
WordPressCMS core50,000+ plugins
Insurance / tax softwareClaims or tax engineOne plug-in per country or state's rules

The parts

Drawing diagram…
PartResponsibility
Core systemThe minimal functionality everyone needs, plus the machinery to find, load and run plug-ins
Plug-in contractThe interface and extension points (hooks) plug-ins can use: the only thing plug-ins may depend on
RegistryKnows which plug-ins exist, their versions, and how to reach them
Plug-insIndependent modules adding features; ideally unaware of each other

Building one: a tiny Markdown processor

1. Define the contract

// core/contract.ts: the ONLY thing plug-ins import from the core
export interface MarkdownPlugin {
  name: string;
  version: string;
  /** Transform raw markdown before parsing (optional hook) */
  beforeParse?(markdown: string): string;
  /** Transform the rendered HTML (optional hook) */
  afterRender?(html: string): string;
}

2. The core: registry + lifecycle

// core/processor.ts
export class MarkdownProcessor {
  private plugins: MarkdownPlugin[] = [];

  use(plugin: MarkdownPlugin): this {
    if (this.plugins.some((p) => p.name === plugin.name)) throw new Error(`Plugin ${plugin.name} already registered`);
    this.plugins.push(plugin);
    return this;
  }

  render(markdown: string): string {
    const source = this.plugins.reduce((text, p) => p.beforeParse?.(text) ?? text, markdown);
    const html = basicMarkdownToHtml(source);                     // the core's own small job
    return this.plugins.reduce((out, p) => p.afterRender?.(out) ?? out, html);
  }
}

3. Plug-ins: independent features

// plugins/emoji.ts
export const emojiPlugin: MarkdownPlugin = {
  name: "emoji",
  version: "1.0.0",
  beforeParse: (md) => md.replaceAll(":rocket:", "🚀").replaceAll(":tada:", "🎉"),
};

// plugins/external-links.ts
export const externalLinksPlugin: MarkdownPlugin = {
  name: "external-links",
  version: "1.2.0",
  afterRender: (html) => html.replace(/<a href="http/g, '<a target="_blank" rel="noreferrer" href="http'),
};

// app.ts
const processor = new MarkdownProcessor().use(emojiPlugin).use(externalLinksPlugin);
processor.render("Launch day :rocket: [docs](https://example.com)");
Drawing diagram…

The Open/Closed Principle at architecture scale: the core is closed for modification, open for extension.

Designing the contract well

GuidelineWhy
Keep the contract small and stableEvery change can break every plug-in in the ecosystem
Version it (semver) and check compatibility on loadOld plug-ins fail loudly instead of mysteriously
Plug-ins depend only on the contract, not on core internals or each otherPlug-ins stay independent and replaceable
Clear, ordered hooks (beforeParse, afterRender)Predictable behaviour when many plug-ins run
Isolate failures: catch plug-in errors, timeoutsOne bad plug-in shouldn't crash the host
Isolate security where plug-ins are third-partySandboxing (VS Code's extension host process, browser permission models)
// Defensive loading: a broken plug-in is disabled, not fatal
for (const plugin of discovered) {
  try {
    assertCompatible(plugin.version, CONTRACT_VERSION);
    processor.use(plugin);
  } catch (error) {
    logger.warn({ plugin: plugin.name, error }, "Plugin disabled");
  }
}

Plug-in discovery

StyleHowExample
Explicit registrationThe app lists plug-ins in code or configvite.config.ts → plugins: [react()]
ConventionLoad everything matching a naming pattern or foldereslint-plugin-*, a plugins/ folder
Manifest + marketplaceEach plug-in ships metadata; a registry installs itVS Code extensions (package.json contributes)
Remote plug-insPlug-ins are services called over HTTP/gRPCWebhooks, Shopify apps

Trade-offs

✅ Strengths❌ Weaknesses
Extensible without touching the coreDesigning a good contract up front is hard
Features can be added, removed or sold separatelyChanging the contract is painful for the ecosystem
Plug-ins are small, focused and testable in isolationPlug-in interactions can be hard to debug
Customisation per customer/region without forksPerformance overhead of indirection and isolation

When to use it

✅ Products that need customisation or third-party extensions (IDEs, CMSs, build tools), rules that vary by customer, country or product line, and features that should be switched on and off independently.

❌ Apps without a clear, stable core or without real variation. A plug-in system nobody plugs into is YAGNI.

Key takeaways

  • Microkernel = a small, stable core plus independent plug-ins connected through a contract.
  • VS Code, browsers, Webpack, ESLint and WordPress are all built this way.
  • Keep the contract small, versioned and stable; plug-ins depend only on it.
  • Isolate plug-in failures (and, for third parties, security).
  • Use it when extensibility and variation are real requirements.