Sealed Interface vs Sealed Class in Kotlin: Key Differences and When to Use Each

A sealed interface in Kotlin defines a restricted type hierarchy without state or constructors, allowing a class to implement multiple sealed hierarchies, whereas a sealed class is a stateful, single-inheritance construct with constructors and backing fields.

Kotlin’s sealed modifier restricts inheritance to a known set of subtypes declared within the same compilation unit. While sealed classes have existed since Kotlin 1.0, sealed interfaces arrived in Kotlin 1.5 as a language feature gated by SealedInterfaces in [LanguageVersionSettings.kt](https://github.com/JetBrains/kotlin/blob/master/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt#L195). Understanding the architectural distinction between these two constructs is essential for designing maintainable type hierarchies.

Core Architectural Differences

The Kotlin compiler treats both constructs as sealed modalities, sharing subtype enumeration logic in [SealedClassInheritorsProvider.kt](https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/resolve/SealedClassInheritorsProvider.kt#L19‑L27). However, their runtime characteristics diverge significantly:

Aspect sealed class sealed interface
State & Constructors Can declare primary/secondary constructors and hold mutable backing fields. Cannot have constructors or backing fields; only abstract members and default implementations allowed.
Inheritance Model Single inheritance only—a class may extend just one sealed class. Multiple inheritance permitted—a class can implement several sealed interfaces, enabling composition of distinct variant groups.
Fun Interface Restriction Can technically be marked fun (though semantically odd). Explicitly forbidden. The compiler emits UNSUPPORTED_SEALED_FUN_INTERFACE via [SealedInterfaceAllowedChecker.kt](https://github.com/JetBrains/kotlin/blob/master/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/SealedInterfaceAllowedChecker.kt#L19‑L22).
Exhaustiveness when expressions are exhaustive if all subclasses are covered. Identical guarantee; the compiler treats sealed interfaces as closed sets (verified in [ExhaustiveSealedInterface.kt](https://github.com/JetBrains/kotlin/blob/master/compiler/testData/diagnostics/tests/when/ExhaustiveSealedInterface.kt)).

Why Choose a Sealed Interface in Kotlin?

Compose Multiple Closed Hierarchies

Because a class can implement multiple interfaces but extend only one class, sealed interfaces enable powerful composition patterns. You can mix independent variant groups—such as sealed interface UiEvent and sealed interface NetworkResult—into a single concrete type without forcing a single inheritance tree.

Model Pure Type Hierarchies Without State

When your domain concept requires no shared mutable state or common initialization logic, a sealed interface is the lighter abstraction. It clearly signals that the hierarchy exists solely for polymorphic behavior, not for shared implementation details.

Avoid Breaking Changes in Future Refactoring

Starting with a sealed interface preserves flexibility. If you later discover that a type needs to participate in another sealed hierarchy, you can simply add another interface implementation. Converting a sealed class to an interface later would require breaking changes to the inheritance structure.

When to Prefer a Sealed Class

Despite the flexibility of sealed interfaces, sealed classes remain the correct choice when:

  • Shared state is required. You need backing fields or a primary constructor to enforce invariant initialization across all variants.
  • Single inheritance is desirable. You want to prevent consumers from mixing your hierarchy with other sealed types, ensuring a strict linear taxonomy.
  • Constructor visibility matters. You need to restrict instantiation via private or protected constructors, which interfaces cannot provide.

Code Examples

Basic Sealed Class with State

sealed class Result {
    data class Success(val data: String) : Result()
    object Loading : Result()
    class Error(val cause: Throwable) : Result()
}

All subclasses must be declared in the same file, and when expressions achieve exhaustiveness without an else branch.

Equivalent Sealed Interface (Stateless)

sealed interface UiState {
    data class Content(val items: List<String>) : UiState
    object Empty : UiState
    object Loading : UiState
}

Note the absence of constructors. Each implementor is a standalone data class or object, yet the compiler still enforces exhaustive when checks as verified in [ExhaustiveSealedInterface.kt](https://github.com/JetBrains/kotlin/blob/master/compiler/testData/diagnostics/tests/when/ExhaustiveSealedInterface.kt).

Composing Multiple Sealed Interfaces

sealed interface NetworkResult {
    object Success : NetworkResult
    data class Failure(val error: String) : NetworkResult
}

sealed interface UiAction {
    object Refresh : UiAction
    data class ShowMessage(val text: String) : UiAction
}

// A concrete class can implement both closed hierarchies
class AppState : NetworkResult, UiAction

This composition is impossible with two sealed classes due to Kotlin’s single-inheritance limitation.

Compiler Enforcement: No Fun Interfaces

// This will not compile
sealed interface Clickable  // Error: UNSUPPORTED_SEALED_FUN_INTERFACE

The compiler rejects this during frontend analysis via [SealedInterfaceAllowedChecker.kt](https://github.com/JetBrains/kotlin/blob/master/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/SealedInterfaceAllowedChecker.kt#L19‑L22), which explicitly prohibits combining sealed with fun interface semantics.

Summary

  • Sealed interfaces (Kotlin 1.5+) define closed type hierarchies without state or constructors, enabling multiple inheritance and composition of variant groups.
  • Sealed classes enforce single inheritance and can hold shared state, constructors, and backing fields.
  • Both constructs share exhaustiveness logic in SealedClassInheritorsProvider.kt, ensuring when expressions remain exhaustive without else branches.
  • Choose sealed interfaces for pure polymorphism and composition; choose sealed classes when you need shared implementation details or strict linear hierarchies.

Frequently Asked Questions

Can a sealed interface have properties with backing fields?

No. A sealed interface cannot declare properties with backing fields or initialize state via constructors. It may only declare abstract properties (which implementing classes must override) or provide default implementations of methods. If you need shared mutable state, use a sealed class instead.

Why does Kotlin forbid sealed fun interfaces?

The combination is explicitly blocked by the compiler checker SealedInterfaceAllowedChecker.kt, which reports UNSUPPORTED_SEALED_FUN_INTERFACE. A fun interface (SAM interface) is designed for single abstract method lambdas, while a sealed type is designed for closed, exhaustive hierarchies. The semantic overlap creates ambiguity in compiler resolution, so the language designers prohibited the combination.

How does the compiler ensure exhaustiveness for sealed interfaces?

The compiler uses the same mechanism for both sealed classes and sealed interfaces. The SealedClassInheritorsProvider.kt component enumerates all direct inheritors registered in the same compilation unit. During flow analysis, the compiler verifies that a when expression covers all known implementors of the sealed interface, producing an error if any variant is missing and no else branch is present.

Can I convert a sealed class to a sealed interface without breaking changes?

Converting a sealed class to a sealed interface is a breaking change if any subclass extends the sealed class, because classes can implement multiple interfaces but extend only one class. All direct subclasses would need to change from : (inheritance) to : (implementation) syntax, and any code relying on the sealed class’s constructors or shared state would fail. If the hierarchy is purely abstract with no shared implementation, migration is feasible but still requires source modifications.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →