DEV Community

kavearhasi v
kavearhasi v

Posted on

Kotlin’s Sealed Interfaces: Smashing the Inheritance Wall

In my last post, we saw how sealed class can shield us from junk-drawer null objects by enforcing compile-time exhaustiveness.

But sealed classes still have their limits. And when I hit that wall, sealed interfaces broke it down.


The Brick Wall: RecyclerView

Imagine a RecyclerView in a news app:

  • HeaderItem for section titles
  • StoryItem for news content
  • AdItem for in-feed ads

Naturally, I want all items under one umbrella type:

sealed class ContentItem {
    data class HeaderItem(val text: String) : ContentItem()
    data class StoryItem(val story: Story) : ContentItem()
    data class AdItem(val adView: View) : ContentItem()
}
Enter fullscreen mode Exit fullscreen mode

Now my adapter can switch over ContentItem exhaustively.

Beautiful.

Until—an ad library arrives with its own AdView subclass that must extend their base AdItemBase.

Suddenly, my world crumbles.

  • Kotlin sealed classes = single inheritance
  • Java = single inheritance
  • Two base classes → 💥 no dice

I was cornered.


Enter Sealed Interfaces

Kotlin 1.5 introduced sealed interfaces.

Unlike sealed classes, they don’t lock you into one inheritance path. Any class can implement multiple sealed interfaces in addition to extending a base class.

So I refactor:

sealed interface ContentItem

data class HeaderItem(val text: String) : ContentItem
data class StoryItem(val story: Story) : ContentItem

// Can extend base class AND implement ContentItem
class AdItem(val adView: View) : AdItemBase(), ContentItem
Enter fullscreen mode Exit fullscreen mode

Suddenly, the wall is gone. My adapter still enjoys exhaustiveness, and I’m free to mix sealed types with third-party hierarchies.


Why Did Sealed Class Fail Here?

It’s not the sealed keyword itself — it’s that sealed classes still follow the JVM’s single-inheritance rule.

You can’t both:

  1. Extend a library base class
  2. And extend your own sealed base class

That’s the trap sealed interfaces escape.


Rule of Thumb

  • Default to sealed interface when modeling hierarchies. It’s flexible and plays nicely with external APIs.
  • Use sealed class when:

    • You need shared state or behavior in the base
    • You want to restrict instantiation (e.g., private constructor)
    • You want companion objects or utility functions tied to the sealed root

Takeaway

sealed class gave us type safety.
sealed interface took it a step further, freeing us from the chains of single inheritance.

My rule now? Default to sealed interface. Reach for sealed class only when shared implementation is essential.

Have you ever been cornered by single inheritance in Kotlin? How did you escape? 👇

Top comments (0)