Representative interview topic

System design interview: How would you design a Wasm Component Model plugin runtime?

System designHard
Offer.cc Editorial TeamPublished Updated

Question

A SaaS platform lets third parties write Rust, Go, or Python plugins that run transformations and validation inside tenant requests. Design a Wasm Component Model plugin runtime covering WIT interfaces, capabilities, compatibility, resource limits, signing, observability, and rollback.

Prompt and context

The platform lets third-party plugins process file conversions, field validation, and tenant-specific rules. Plugins come from different languages and cannot be trusted directly; one runaway plugin must not take down the host process. Design a Wasm Component Model runtime covering interface definition, loading, isolation, resource budgets, compatible upgrades, and recovery.

The Component Model describes imports and exports with components, interfaces, and worlds. WIT is the interface definition language, while the Canonical ABI defines how higher-level values cross language boundaries. The interview tests whether you can turn those standards into an operable multi-tenant plugin platform.

What the interviewer evaluates

Cover interface and type governance, least privilege, instance isolation, CPU and memory budgets, timeout and cancellation, component signing and provenance, version compatibility, reproducibility, logs and metrics, canaries, and rollback.

Clarifying questions to ask

  • Does a plugin run inside a synchronous request or an asynchronous job, and what are latency and throughput budgets?
  • Which file, network, key, or clock capabilities are required, and how are tenants isolated?
  • May the interface use resources, streams, and futures, or only bounded value types?
  • Must old tenants remain reproducible after upgrades, and how long is the compatibility window?
  • Should a violation or timeout terminate immediately, retry in isolation, or use a host fallback?

A 30-second answer

“Define a narrow, versioned WIT world and expose only required business capabilities. The control plane verifies provenance, signatures, and dependencies; the data plane runs an isolated Wasm instance with CPU, memory, output, and deadline limits. Check interface compatibility and canary upgrades, while recording version, tenant, budget consumption, and result summaries. A timeout or violation trips only that plugin and returns a safe host default.”

Step-by-step deep dive

Step 1: Define WIT interfaces and data boundaries

Prefer bounded records, variants, lists, and results with explicit error enums, maximum lengths, and encoding. Do not expose host objects or hidden global state; each world should contain only the imports and exports required by that plugin class.

text
world transform-v2 {
  export transform: func(input: record { bytes: list<u8>, mime: string }) -> result<list<u8>, transform-error>
}

The interface registry records package name, version, compatibility rules, and binding-toolchain version. Define a separate backpressured stream interface for large data rather than disguising unbounded input as a list.

Step 2: Build capability and tenant isolation

Create a fresh instance per call. By default provide no network, filesystem, environment, randomness, or key capabilities. When access is required, grant an explicit host function bound to tenant, plugin, and request identity.

Capability tokens should be short-lived, auditable, and non-replayable across tenants. Keep host APIs minimal; never hand a plugin host-process pointers or unfiltered system handles.

Step 3: Set budgets and termination

Set instruction or time budgets, linear-memory limits, table and stack limits, output caps, and concurrency quotas. Propagate cancellation with a deadline; terminate an instance that exceeds its budget and record the reason. Decide whether a retry is safe only after checking idempotency.

Use an asynchronous queue and lease for long jobs instead of keeping an unbounded wait inside a user request. Meter and rate-limit by both tenant and plugin so one tenant cannot consume the shared execution pool.

Step 4: Handle component and ABI compatibility

Before release, parse the WIT world and dependencies and check imports and exports against compatibility rules. The Canonical ABI standardizes value representation but does not guarantee business compatibility; enum additions, changed error meanings, and unit changes still require contract tests.

Keep executors and bindings for old worlds and let plugins declare supported interface versions. Use dual reads or shadow execution to compare results and resource changes before switching the default version.

Step 5: Verify supply chain and loading

Generate an immutable artifact digest and sign the component, WIT world, dependency lockfile, and build metadata. The control plane allows only trusted signers and scanned dependencies; loading rechecks digest, signature, target runtime version, and revocation state.

Record registration, approval, revocation, and rollback in an audit log. Never download an unregistered component at runtime. Address the cache by digest so a moved tag cannot silently change the code being executed.

Step 6: Observe, canary, and recover

Record plugin digest, interface version, tenant, latency, resource use, result status, and error class for every call. Logs must not contain user payloads or keys. Aggregate metrics by plugin and tenant, including timeouts, memory exhaustion, and rejection rate.

Canary a new version on synthetic traffic or a small tenant set and compare result deltas and tail latency. On regression, stop and revoke that version and return to the previous digest. The host supplies a safe default so a plugin failure does not spread into core business logic.

A strong sample answer

I would define a narrow, versioned WIT world with bounded types and grant only required host capabilities. Each request gets an isolated instance with CPU, memory, output, deadline, and concurrency limits; the control plane checks signatures, digests, dependencies, and revocation. Upgrades pass contract checks, shadow execution, and a small canary. Observability is split by plugin, tenant, and version; a timeout or violation kills only that instance and returns a safe host default.

Common mistakes

  • Exposing host objects directly → privilege escalation and cross-tenant leakage → use minimal host functions.
  • Limiting memory but not time → an infinite loop still consumes the pool → set CPU, deadline, and concurrency budgets.
  • Treating Canonical ABI as business compatibility → units and error semantics can still break callers → keep contract tests and world versions.
  • Caching by mutable tag → a moved tag runs unknown code → address by immutable digest and signature.
  • Retrying every failure → non-idempotent side effects repeat → declare idempotency and classify errors.

Follow-up questions and responses

Follow-up 1: Why not use processes or containers?

It depends on the threat model and budget. Wasm instances start quickly and expose controlled interfaces, but they do not replace runtime patching, host hardening, or defense in depth; high-risk plugins can still run in a stronger isolation layer.

Follow-up 2: How do you support streaming large files?

Define a backpressured stream interface with limits on concurrency, chunk size, and cumulative output. Use asynchronous jobs for long tails and audit cancellation and lease state.

Follow-up 3: How do you decide whether two versions are equivalent?

Run a fixed input corpus through shadow executions and compare normalized results, error classes, latency, and resource use. Allow declared floating-point or ordering differences and gate release on unacceptable deltas.

Follow-up 4: What if a plugin needs network access?

Give it a tenant- and domain-allowlisted proxy capability with timeouts, response-size limits, and audit records. Deny arbitrary addresses by default and invalidate capability tokens immediately on revocation.

Public sources

Related questions

Related interview tool

Use Solve for a system design answer

Clarify the requirements first, then move through scale, architecture, component choices, and trade-offs.

View the tool