Interview QuestionPractical QuestionFollow-up Questions

Sealed Classes and Restricted Hierarchies

skydovesJaewoong Eum (skydoves)||9 min read

Sealed Classes and Restricted Hierarchies

Kotlin sealed classes define a closed set of subtypes that are all known at compile time, enabling the compiler to verify that pattern matching covers every possible case. Unlike open classes where new subclasses can appear in any module, sealed classes restrict their direct subclasses to the same package, giving the compiler complete knowledge of the hierarchy. This property makes sealed classes the foundation for modeling finite states, result types, and protocol messages in Kotlin applications. By the end of this lesson, you will be able to:

  • Explain how sealed classes restrict subclass definition and why this matters for exhaustiveness checking.
  • Describe the compiler's behavior when a when expression covers all sealed subtypes.
  • Compare sealed classes, sealed interfaces, and enum classes for modeling restricted types.
  • Identify common patterns where sealed classes improve type safety.
  • Apply sealed hierarchies to model UI state, network results, and navigation events.

Subclass Restriction and Exhaustiveness

A sealed class requires all direct subclasses to be defined in the same package as the sealed class itself. Since Kotlin 1.5, subclasses no longer need to be in the same file, but they must share the package. This constraint gives the compiler a complete list of all possible subtypes at every usage site:

sealed class NetworkResult {
    data class Success(val data: String) : NetworkResult()
    data class Error(val message: String) : NetworkResult()
    data object Loading : NetworkResult()
}

When you use a sealed class in a when expression and cover every subtype, the compiler knows the match is exhaustive. No else branch is needed, and adding a new subtype later produces a compile error at every when site that does not handle it:

fun render(result: NetworkResult) = when (result) {
    is NetworkResult.Success -> showData(result.data)
    is NetworkResult.Error -> showError(result.message)
    NetworkResult.Loading -> showSpinner()
    // No else needed; compiler verifies all cases
}

The compile time error on unhandled cases is the primary advantage over open class hierarchies. With an open class, the compiler must allow an else branch because unknown subtypes might exist. With a sealed class, the compiler eliminates that uncertainty.

Sealed Classes vs Enum Classes

Enum classes also provide exhaustiveness checking, but they differ from sealed classes in two important ways. Each enum constant is a singleton. You cannot have two instances of the same enum value with different data. Sealed class subtypes can be regular classes with their own constructors and state:

// Enum: each entry is a singleton, no per-instance data
enum class Direction { NORTH, SOUTH, EAST, WEST }

// Sealed: subtypes carry distinct data shapes
sealed class Route {
    data class Named(val name: String) : Route()
    data class Coordinates(
        val lat: Double, val lng: Double
    ) : Route()
    data object Unknown : Route()
}

This interview continues for subscribers

Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.

Become a Sponsor