skip to content

How does polymorphic creation scale to a plugin or extension architecture, and what changes when the concrete implementations are chosen at runtime by configuration or third-party code?

level: principalimportance: nice to knowfreq 26%

answer

  1. subclass = compile-time choice; registry = data-driven
  2. extension by registration, not modification
  3. errors move to runtime → fail fast at startup
  4. provider interface becomes versioned public API
  5. in-process plugins are a trust boundary

basics

~20 s

Subclass-based creation can't pick an implementation from a config string, so systems move to a registry: a map from key to creator, filled at startup by wiring or plugin discovery. Extension becomes registration, and errors move from compile time to runtime.

solid answer

~50 s

Factory Method varies creation by the creator's *type*, which is fixed at compile/wiring time. Plugin systems need selection by *data* (a config key, a URL scheme, a message type) and by code the host doesn't compile against. The generalization is a **registry of factories**: `Map<Key, Creator>` populated at startup — explicitly, by a DI container's multi-binding, or by discovery (service-loader style manifests, annotation scanning, dynamic module loading). What changes at that boundary: (1) failures become runtime — unknown key, duplicate key, missing plugin — so you need explicit defaults, fail-fast startup validation, and clear diagnostics; (2) the creation interface becomes a **published contract** and must be versioned, because third parties compile against it; adding a method is now a breaking change, so prefer narrow interfaces plus capability negotiation; (3) trust and isolation matter — plugin code runs in-process, so consider sandboxing, resource/time limits, and treating plugin output as untrusted; (4) observability: log which implementation was selected and expose it in health/diagnostics.

go deeper

for a junior

Note that picking an implementation from a config value needs a lookup table (key → creator), not a subclass.

for a middle

Describe the registry, how it preserves Open/Closed while staying data-driven, and that unknown-key errors now appear at runtime.

for a senior

Cover population strategies, startup fail-fast validation, duplicate-key policy, lifetime/thread-safety contracts, and lazy creation trade-offs.

for a principal

Treat the extension point as a governed, versioned public contract and trust boundary: API evolution strategy, isolation options up to out-of-process extensions, observability requirements, and the judgement of how far up the decoupling ladder to climb.

## Why the subclass form runs out of road Factory Method binds the creation decision to the creator's *static type*. A plugin architecture needs the decision bound to *runtime data* and to code the host was never compiled against: - "Use the `s3://` storage backend" — a scheme string from a URL. - "Handle message type `OrderPlaced.v3`" — a header value. - "Load whatever payment providers the operator dropped into the extensions directory." None of these can be expressed by choosing a subclass at compile time. ## The generalization: registry of creators ``` interface StorageFactory { fun scheme(): String; fun create(cfg: Config): Storage } registry: Map<String, StorageFactory> // populated at startup fun open(uri) = registry[uri.scheme]?.create(cfg) ?: fail("no storage provider for scheme '${uri.scheme}'") ``` The conditional of a simple factory is replaced by a lookup table; the inheritance of Factory Method is replaced by composition. Extension = **registration**, not modification, so Open/Closed survives while selection stays data-driven. Population strategies, roughly in order of dynamism: | Strategy | How | Trade-off | |---|---|---| | Explicit registration in the composition root | `register("s3", S3Factory())` | Fully static and greppable; requires a code change per plugin | | DI multi-binding / collection injection | Container injects `Set<StorageFactory>` | Idiomatic, testable; container-specific behaviour | | Service-loader / manifest discovery | Providers declare themselves in metadata on the classpath/module path | Third parties add a jar/package with no host change; classloading and ordering subtleties | | Dynamic module loading | Load code from a directory at runtime | Maximum flexibility; hardest isolation, versioning, and security story | ## What genuinely changes at a published extension boundary ### 1. Failure timing and diagnosability Compile-time errors become runtime ones: unknown key, duplicate key (two providers claim `s3`), missing dependency, provider that throws during construction. Countermeasures: - **Fail fast at startup**: validate that every configured key resolves, rather than discovering it on the first request. - **Deterministic conflict policy**: last-wins, priority attribute, or hard error — decide and document it, never leave it to classpath order. - **Actionable messages**: list the keys that *are* registered when one is missing. - **Sensible default/no-op provider** where degradation beats a crash. ### 2. The creation interface becomes a versioned public API Once third parties implement `StorageFactory`, adding a method breaks every existing plugin (the Abstract Factory add-a-product problem, now across an organizational boundary). Practices: keep the interface minimal (Interface Segregation); add capability sub-interfaces rather than methods (`if (f is SupportsResume) …`); provide default implementations where the language allows; declare a plugin API version and refuse incompatible plugins with a clear message; keep host-internal types out of the signature so the surface stays small. ### 3. Lifetime, threading, and configuration Decide and document: is the factory a singleton and the product per-call? Must implementations be thread-safe? Who owns closing/releasing products? Is creation allowed to block or do I/O (matters if it happens on a request path)? Registries also invite **lazy creation** — store a supplier and construct on first use, which prevents a broken or expensive provider from delaying startup, at the cost of moving failures later (so pair with a startup self-check). ### 4. Trust boundary and isolation Plugin code typically runs **in-process with full privileges**: it can exhaust memory, block threads, swallow errors, or exfiltrate data. Mitigations: separate classloaders/modules with restricted visibility, timeouts and circuit breakers around calls into plugins, treating returned data as untrusted input, signing/allow-listing plugin artifacts, and — the strongest option — moving extensions out of process behind a protocol boundary (subprocess, RPC, WebAssembly), trading latency for real isolation. Discovery that scans the classpath is itself an attack surface if untrusted artifacts can be placed there. ### 5. Observability and operability Log the resolved implementation and its version at startup and expose it in health/info endpoints; emit per-provider metrics; make "which provider handled this?" answerable from a trace. Without this, dynamic selection turns every incident into an archaeology exercise. ## Architectural framing This is the **plugin / microkernel** style: a stable core defines contracts and a registry; volatile capabilities live behind them. Factory Method is the in-process, single-team seed of the same idea. The progression — direct constructor → factory method → injected factory object → keyed registry → discovered, versioned, isolated plugin API — is a ladder of increasing decoupling *and* increasing operational cost. The senior judgement call is climbing only as far as the actual variability and ownership boundaries justify: each rung buys extensibility and pays in traceability, failure-time, and governance.

  • Two plugins register themselves under the same key. What should the system do?
    Apply a documented, deterministic policy rather than depending on discovery order: fail fast at startup naming both artifacts (safest for correctness-critical providers), or resolve by an explicit priority/version attribute with a warning. Silent last-wins is the worst option because behaviour then depends on classpath ordering.
  • Why does adding a method to a published provider interface deserve more caution than adding one to an internal abstract creator?
    External implementors compile against it, so the change breaks every third-party plugin and can't be fixed by editing your repository. Prefer optional capability sub-interfaces, language-level default implementations, or an explicitly versioned plugin API with compatibility checks at load time.

Factory Method is a company where each branch office builds its own product. A registry is a switchboard: callers ask for "shipping", and whoever registered under that name answers. Once outside vendors can register, you need a rulebook — who may register, what happens on a name clash, what the switchboard promises not to change.

saying these in an interview costs you the question

  • Assuming subclass-based Factory Method can select an implementation from a configuration string without a registry.
  • Relying on classpath/discovery order to resolve duplicate registrations.
  • Treating a provider interface as freely changeable after third parties implement it.
  • Ignoring that in-process plugins execute with the host's privileges and can hang or crash it.
  • Deferring all resolution to first use with no startup validation, so misconfiguration surfaces as a production request failure.
  • Climbing to a full plugin architecture when one team owns all implementations.

context