skip to content

When should you reach for an extension function versus a member function or a utility class, and what are the design trade-offs?

level: middleimportance: should knowfreq 50%

answer

  1. Don't own the type or want a lean class → extension
  2. Needs private state or must be overridable → member
  3. Extensions read better than Util.doX(obj)
  4. Static dispatch: not virtual, member wins on clash
  5. No access to privates; discoverability via imports

basics

~20 s

Use an extension when you want a method-like helper on a type you don't own or want to keep the class small, and the helper only needs the type's public API. Use a member when it needs private state or should be overridable.

solid answer

~50 s

Choose an **extension** when: you cannot modify the type (JDK/library/third-party); you want fluent, dot-callable helpers without bloating the class; the logic only needs the receiver's **public** API; or you want the helper scoped/imported per call site. Choose a **member** when the function needs **private state**, must be **polymorphic/overridable** (extensions dispatch statically and can't be overridden), or is core to the type's contract. Extensions beat old-style `Util.doX(obj)` classes because they read left-to-right (`obj.doX()`) and chain naturally. Trade-offs: extensions are static, so no virtual dispatch and no access to privates; they can be **shadowed by a later-added member** with the same signature (members win), which is an evolution risk for libraries; and discoverability depends on imports and IDE support. Keep extensions cohesive (group them per file), avoid extending types you don't control with overly generic names, and don't use extensions to fake encapsulation you actually need.

go deeper

for a junior

Knows extensions are good for helpers on types you can't modify.

for a middle

Compares extensions to members and util classes and cites readability plus the no-private-access limit.

for a senior

Weighs static dispatch, non-overridability, and member-wins shadowing as concrete design trade-offs.

for a principal

Sets team conventions: where extensions belong, naming/scoping discipline, and avoiding fragile extensions on externally owned types.

## Decision guide ### Reach for an extension function when - **You don't own the type** — a JDK class, a library type, generated code. You can't add members, but you can add extensions: `fun LocalDate.isWeekend() = dayOfWeek in setOf(SATURDAY, SUNDAY)`. - **You want to keep the class lean** — push convenience/derived helpers out of the core class so its public surface stays focused. - **The logic only needs the public API** of the receiver. - **You want call-site scoping** — extensions are imported, so a helper can exist only where it's relevant. - **Readability/chaining** — `data.trimmed().normalized().shout()` reads better than nested `Util` calls. ### Prefer a member function when - **It needs `private`/`protected` state.** Members can; top-level extensions cannot. - **It must be polymorphic.** Members are virtual and `override`-able; extensions are resolved on the **static type** and cannot be overridden — subclasses can't specialize them. - **It's part of the type's essential contract**, not a peripheral convenience. ## Why extensions beat utility classes ```kotlin // Old Java-ish style StringUtil.shout(StringUtil.trimQuotes(s)) // Extension style s.trimQuotes().shout() ``` Extensions restore method-call ergonomics and autocompletion on the receiver while keeping the implementation outside the class. ## Key trade-offs and pitfalls - **Static dispatch:** extensions are not virtual. Don't use them where you expect a subtype to override behavior. - **Member-wins shadowing (evolution risk):** if the receiver class later gains a **member** with the same name/signature as your extension, the **member is chosen** at compile time and your extension is silently bypassed at those call sites. This is a real risk when extending types you don't control. (The detailed precedence rules are a separate topic.) - **No private access:** extensions can't reach encapsulated state — sometimes that's a signal the logic belongs *in* the class. - **Discoverability:** they live in other files and require imports; cohesive grouping and clear naming matter. ## Rules of thumb - Convenience/derived/format helpers → extension. - Needs private state or polymorphism → member. - Cross-cutting helpers on owned types → either; prefer members if they're truly part of the contract. - Group related extensions per file/package; avoid hyper-generic names on widely used types (e.g. don't add `fun Any.process()`).

  • What happens if the receiver class later adds a member matching your extension's signature?
    The member takes precedence at compile time, so existing call sites silently switch to the member and bypass the extension — an evolution hazard for extensions on types you don't control.
  • Why can't you rely on an extension being overridden by a subclass?
    Extensions dispatch on the static type and are not virtual, so subclasses cannot override them; only member functions are polymorphic.

Members are built-in features of an appliance; extensions are accessories you clip on — handy, but they can't rewire the internals.

saying these in an interview costs you the question

  • Using extensions where polymorphism/overriding is required
  • Adding broad extensions on Any/common types with generic names
  • Expecting an extension to access encapsulated private state
  • Ignoring the member-wins shadowing risk for library types
  • Replacing all members with extensions 'for style' with no rationale

context