skip to content

When designing the contract between a microkernel core and its plug-ins (the interface plug-ins must implement), what belongs in that contract and what design choices affect how safely plug-ins can be added later?

level: middleimportance: must knowfreq 65%

answer

  1. identity + lifecycle + functional layers
  2. interface segregation for extension points
  3. version the contract, not just the plug-in
  4. no core-internal types on the boundary

basics

~20 s

The contract is the fixed set of methods and data a plug-in must provide so the core can call it generically. Keep it small, stable, and versioned so new plug-ins can be added without changing old ones or the core.

solid answer

~40 s

The contract typically includes an identity/metadata block (id, version, declared capabilities), lifecycle hooks (init, start, stop, dispose) so the core can manage the plug-in's lifetime, and the functional interface the plug-in implements to do its actual work. Good contracts favor small, cohesive interfaces (interface segregation) over one giant 'do everything' interface, and use extension points rather than hard dependencies — a plug-in declares what it provides and what it needs, and the core wires things up. Contracts should be versioned explicitly so the core can support multiple plug-in API versions simultaneously during a migration, and should avoid leaking core-internal types into the plug-in-facing API, since that couples every plug-in to core internals.

go deeper

for a junior

Should recognize that a contract is an interface plug-ins implement, and give a simple example of a method it might expose.

for a middle

Should explain lifecycle hooks and why small, focused interfaces are preferred over one large interface.

for a senior

Should discuss contract versioning strategy and the danger of leaking core-internal types across the plug-in boundary.

for a principal

Should reason about long-term API governance across a plug-in ecosystem: deprecation windows, supporting multiple contract versions simultaneously, and dependency resolution between plug-ins.

## Why the contract is the hard part The contract is the single most consequential design artifact in a microkernel system, because unlike an internal class boundary that one team can refactor freely, the contract is a **promise made to every plug-in author** — including third parties who may never talk to the core team directly. Getting it wrong is expensive to fix later, so it deserves the same rigor as a public API or a wire protocol. ## The three layers Concretely, a plug-in contract usually has three layers. 1. **First, an identity/metadata layer**: every plug-in declares who it is (an id), what version it is, what version(s) of the core API it targets, and what capabilities or extension points it fills. This metadata is what the core's registry uses to decide compatibility and resolve dependencies between plug-ins — OSGi's bundle manifest and Eclipse's `plugin.xml` are concrete, long-lived examples of this layer. 2. **Second, a lifecycle layer**: hooks like `init()`, `start()`, `stop()`, and `dispose()` that let the core manage the plug-in's lifetime deterministically — allocate resources on start, release them on stop, so plug-ins can be hot-loaded and unloaded without leaking file handles, threads, or memory. 3. **Third, the functional layer**: the actual interface(s) the plug-in implements to do its real work — an `ExportFormat`, a `LanguageServer`, a `Rule`. This is the part application code calls at runtime through the core's dispatch mechanism. ## Small interfaces over one large one A key design discipline is favoring several small, cohesive interfaces over one large interface a plug-in must implement in full — this is the **Interface Segregation Principle** applied to plug-in contracts. If the contract bundles unrelated concerns (say, both 'render output' and 'validate input' methods) into a single interface, a plug-in that only wants to do validation is forced to implement or stub rendering methods it doesn't need, and every future addition to the interface breaks every plug-in even if most don't use the new method. Splitting these into separate extension points that a plug-in opts into independently keeps the blast radius of any one addition small. ## Versioning the contract Versioning is the other central concern, because the core cannot force every plug-in to upgrade in lockstep with it. Mature plug-in platforms solve this by publishing the contract itself as a versioned artifact (v1, v2 extension points) and keeping the core capable of loading plug-ins written against older versions, at least for a defined support window — Eclipse's platform APIs and IntelliJ's plugin SDK both do this, deprecating old extension points gradually rather than deleting them outright. Without this discipline, every core release becomes a synchronized 'flag day' that forces every plug-in author to update simultaneously, which does not scale once there is a real ecosystem of independent plug-in authors. ## What crosses the boundary - **A subtler but important choice is what types cross the contract boundary.** If the contract's method signatures reference core-internal classes (an internal `DocumentImpl`, a core-specific `InternalContext`), then every plug-in becomes coupled to core internals whether or not the core team intended that — refactoring those internals later silently breaks plug-ins. The fix is to define contract-facing types (DTOs, narrow interfaces) that are explicitly part of the public API surface and are kept stable independently of how the core happens to be implemented inside. This mirrors the general API-design rule of never leaking implementation types across a stability boundary. - **Dependency declaration between plug-ins is worth calling out separately**: if plug-in B needs a capability plug-in A provides, the contract should let B declare that dependency explicitly (by id/version) rather than B reaching into A's classes directly. The core, or a dedicated dependency-resolution component (as OSGi has), can then check at load time whether required dependencies are present and at compatible versions, failing fast with a clear error instead of a confusing missing-class error or a null service reference discovered at first use. ## How contracts fail in production In production, contract mistakes show up as: - **version-mismatch crashes** when a plug-in built against contract v1 gets loaded by a core that only implements v2 semantics; - **'god interface' contracts** that force every plug-in to implement dead methods, making tests and mocks noisy; and - **silent coupling to internals** that turns a routine core refactor into a plug-in-breaking release. The mitigations — small segregated interfaces, explicit versioning with a deprecation window, and no internal types on the public boundary — are the same practices that make any long-lived public API sustainable, just applied at the plug-in boundary specifically.

  • Why is it risky for a plug-in's contract methods to accept or return the core's internal domain classes directly instead of dedicated DTOs?
    Because it silently couples every plug-in to the core's implementation details, so a routine internal refactor of that class becomes a breaking change for the entire plug-in ecosystem. Dedicated boundary types (DTOs or narrow interfaces) let the core evolve its internals freely as long as the boundary type's shape stays stable.
  • How does declaring plug-in-to-plug-in dependencies explicitly by id and version help compared to letting plug-ins reference each other's classes directly?
    It lets the core or a resolver validate at load time whether required plug-ins are present and version-compatible, failing with a clear error instead of a runtime null-reference or missing-class error discovered deep in execution. It also lets the core compute a safe load order and detect circular or missing dependencies before anything runs.

Like a standardized electrical socket spec: appliance makers only need to match the plug shape and voltage — they never need to know the wiring inside your walls, and the spec is versioned so old appliances aren't instantly obsoleted.

saying these in an interview costs you the question

  • Designs one giant interface every plug-in must fully implement
  • No mention of versioning the contract itself
  • Lets plug-in methods return core-internal implementation classes
  • Assumes plug-ins can just import each other directly with no declared dependency

context