skip to content

You're designing a public library API. Argue when exposing behavior as an extension (statically resolved) is the right call versus an open member (virtually dispatched), and what binary/source compatibility and override risks each choice carries.

level: principalimportance: nice to knowfreq 18%

answer

  1. Subtypes must specialize? -> open member
  2. Closed, uniform helper? -> extension
  3. Member silently shadows extension
  4. Converting member<->extension flips dispatch
  5. Extensions keep inheritance contract small

basics

~10 s

Use extensions for convenience helpers that shouldn't be overridden or pollute the type, and where uniform per-declared-type behavior is fine. Use open members when subtypes must customize behavior polymorphically.

solid answer

~50 s

The decision hinges on whether subtypes must **specialize** behavior. **Open members** give virtual dispatch: subtypes `override` and callers automatically get specialized behavior through a base-typed reference — essential for polymorphic contracts (templates, strategy points). But every `open` member is an extension point you must support forever, and adding/removing one affects the vtable and inheritance. **Extensions** are statically resolved, can't be overridden, and don't widen the inheritance contract — ideal for stateless convenience helpers, fluent operators, and adapters where you want a stable, uniform mapping from declared type to behavior and you explicitly **don't** want subclasses changing it. Risks: a same-signature member added later **shadows** your extension (silent behavior change); converting member<->extension flips dispatch semantics and is a source-compatibility break for subclassers; extensions on supertypes are invisible through subtype references and vice versa. Prefer extensions to keep the surface closed and predictable; reserve `open` members for deliberate polymorphism.

go deeper

for a junior

Can state extensions are static and members can be overridden, but unlikely to reason about API compatibility.

for a middle

Picks the right tool for simple cases (helper vs polymorphic hook) but may miss shadowing/compat nuances.

for a senior

Weighs polymorphism needs against surface stability and cites the shadowing and conversion hazards.

for a principal

Frames a durable policy: when to expose members vs extensions, the binary/source compatibility implications, and guardrails against silent dispatch flips across versions.

## The fundamental fork Every behavior you expose resolves one of two ways: - **Open member -> virtual dispatch.** Resolved at runtime on the object's actual class. Subtypes can `override`. Callers using a base-typed reference get the subtype's behavior automatically. - **Extension -> static resolution.** Resolved at compile time on the declared receiver type. Cannot be overridden, only **shadowed**. Behavior is fixed per declared type. ## When extensions win - **Stateless convenience / fluent helpers** (`String.toSlug()`, `List<T>.secondOrNull()`): no need for subtype specialization; you want a stable, uniform mapping. - **Adapters over types you don't own**: you can't add members anyway. - **Closed behavior by design**: you explicitly don't want subclasses changing semantics — static resolution guarantees the declared type's version runs. - **Keeping the inheritance contract small**: extensions don't add `open` extension points you must support across versions. ## When open members win - **Polymorphic contracts**: template methods, lifecycle hooks, strategy points where subtypes must customize and callers rely on virtual dispatch through a base reference. - **Behavior that should follow the object, not the variable's type** — exactly what static extensions can't do. ## Compatibility and override risks - **Member-shadows-extension:** if you (or a subclass author) later add a member with the same signature as an existing extension, the member silently wins for dot calls. Code that depended on the extension changes behavior with no compile error. Audit before adding members near popular extensions. - **Converting member <-> extension is a semantic break:** dispatch flips from virtual to static (or vice versa). Subclassers who overrode a member lose their override if it becomes an extension; this is a source- and behavior-compatibility break even if it still compiles for non-subclassers. - **Visibility across the hierarchy:** extensions on a supertype aren't candidates through... actually they are visible on subtype references, but subtype-only extensions are **invisible** through supertype references. Choosing the receiver type wrong strands callers. - **Binary compatibility:** adding/removing `open` members touches the vtable and the inheritance contract; adding top-level extensions is additive and safer, though it can introduce resolution ambiguity at call sites that newly import both. ## A decision checklist ```text Must subtypes customize this behavior, observed through a base reference? YES -> open member (accept the long-term extension-point cost) NO -> extension (closed, static, smaller contract) Is the receiver a type you don't own? -> extension (only option) Do you want to forbid override of this helper? -> extension Is there any same-signature member, now or planned? -> avoid the clash; pick one ``` ## Takeaway Extensions trade polymorphism for a smaller, more stable, override-proof surface; open members trade surface stability for genuine subtype specialization. Decide by whether the runtime object — or the declared type — should drive behavior, and document the choice so refactors don't silently flip dispatch.

  • Why is turning a public open member into an extension a compatibility hazard even if it still compiles?
    Existing subclasses' overrides stop taking effect (dispatch becomes static), silently changing behavior for base-typed callers — a behavioral break.
  • How can adding a new member to a class break callers relying on an extension?
    If the new member shares the extension's signature, member-vs-extension precedence makes the member win, silently shadowing the extension at dot call sites.

saying these in an interview costs you the question

  • Recommending extensions for behavior that subtypes must override
  • Ignoring the member-shadows-extension hazard
  • Treating member<->extension conversion as a safe, transparent refactor
  • Claiming extensions participate in polymorphism
  • No mention of receiver-type visibility across the hierarchy

context