skip to content

What is the design rationale for making Kotlin classes final by default, and what are the trade-offs versus Java's open-by-default model?

level: principalimportance: should knowfreq 30%

answer

  1. Effective Java item 19: design for inheritance or prohibit
  2. Fragile base class problem = self-use coupling
  3. Java open default = implicit unbounded contract
  4. Trade-off: framework friction, third-party rigidity
  5. Prefer interfaces, sealed, delegation (by)

basics

~10 s

Final-by-default protects against accidental inheritance and the fragile base class problem, pushing teams toward composition and interfaces. The trade-off is more friction when frameworks or extension genuinely need subclassing.

solid answer

~40 s

Kotlin makes classes final by default to encode Effective Java's advice: "design and document for inheritance, or else prohibit it." Open-by-default (Java) lets anyone subclass any class, creating an *implicit, unbounded inheritance contract*: the base author must keep internal call sequences stable forever or risk breaking subclasses — the fragile base class problem. Final-by-default flips the burden: extension is a conscious opt-in (`open`), so the public inheritance surface is small and intentional, favoring composition and interfaces. Trade-offs: friction with proxy/mock frameworks (mitigated by all-open / kotlin-spring / MockK), and occasional inability to extend a third-party class you don't control. The pragmatic stance: prefer interfaces and `open` only true extension points; reach for plugins for infrastructure; accept slightly more upfront design effort for long-term API stability.

code

kotlin · 4 lines
kotlin
// Can't extend a final library class -> wrap it via delegation
class LoggingList<T>(private val inner: MutableList<T>) : MutableList<T> by inner {
    override fun add(e: T): Boolean { println("add $e"); return inner.add(e) }
}

go deeper

for a junior

States that final-by-default prevents accidental subclassing.

for a middle

Connects it to safer APIs and knows frameworks need the all-open plugin.

for a senior

Articulates the fragile-base-class rationale and the concrete trade-offs with mitigations.

for a principal

Frames it as API governance, prescribing a posture (interfaces, sealed, delegation, plugins) and weighing flexibility against long-term compatibility.

## The rationale The core motivation is **Effective Java, Item 19: "Design and document for inheritance or else prohibit it."** Inheritance couples a subclass to the *implementation* of its superclass, not just its public contract. If the base class later changes which of its own methods it calls internally (self-use), subclasses that overrode those methods can silently break. This is the **fragile base class problem**. Java's default is **open**: every class is subclassable unless marked `final`. In practice almost no one marks classes final, so the entire codebase carries an implicit, unbounded inheritance contract. Kotlin **inverts** the default so that allowing inheritance is an explicit, documented decision (`open`). ## What final-by-default buys you - **Smaller, intentional inheritance surface** — only classes meant to be extended are. - **Safer evolution** — a final class's internal refactors can never break external subclasses (there are none). - **Nudge toward composition / interfaces** — "prefer composition over inheritance" becomes the path of least resistance. - **Optimization headroom** — final methods are easier for the compiler/JIT to devirtualize/inline. ## The trade-offs - **Framework friction**: proxy- and mock-based tools subclass at runtime and fail on final classes. Mitigations: **all-open** / **kotlin-spring** compiler plugins, and **MockK** / **mockito-inline** for testing. - **Third-party rigidity**: you cannot extend a final library class you don't own; you must wrap (delegation) instead. Kotlin's `by` interface delegation helps here. - **Upfront design cost**: authors must decide extension points consciously rather than "leave it open just in case." ## Principal-level framing This is fundamentally an **API-governance** choice: it trades a little day-one flexibility for durable backward compatibility and clearer contracts. The recommended posture: 1. Default to **final** + **interfaces** for polymorphism. 2. Use **`open`** only for documented template-method hooks. 3. Use **`sealed`** when the subclass set is closed and known (exhaustive `when`). 4. Lean on **delegation (`by`)** instead of inheriting third-party classes. 5. Enable plugins for infrastructure rather than hand-opening domain code. ```kotlin // Composition over inheriting a final third-party class class LoggingList<T>(private val inner: MutableList<T>) : MutableList<T> by inner { override fun add(element: T): Boolean { log(element); return inner.add(element) } } ```

  • If you can't extend a final third-party class, what's the idiomatic alternative?
    Composition via delegation — wrap the instance and use Kotlin's `by` to forward an interface, overriding only what you need.
  • Does final-by-default help performance?
    It can: final methods are non-virtual, making them easier for the JIT to inline/devirtualize, though this is a secondary benefit.

Java leaves every room unlocked; Kotlin locks them and the author hands out keys (open) only for rooms meant to be entered.

saying these in an interview costs you the question

  • Calling final-by-default a mistake with no upside
  • Not mentioning the fragile base class problem
  • Unaware of composition/delegation as the alternative to inheritance
  • Thinking the only motivation is performance

context