A sealed class in Kotlin is an abstract class that restricts the hierarchy of subclasses: all subtypes must be declared in the same file. This is convenient for hierarchies where the set of options is fixed (for example, for describing states, events, or errors):
sealed class Result { object Success : Result() data class Error(val message: String) : Result() object Loading : Result() } fun handle(result: Result) = when (result) { is Result.Success -> print("Success!") is Result.Error -> println("Error: ${result.message}") is Result.Loading -> println("Loading...") }
Relation to when:
If we use all subtypes of a sealed class in a when expression, the compiler checks for exhaustiveness. This guarantees that when a new state is added, the developer will receive a compilation error if the new case is not handled.
Why this is important:
"Can subclasses of a sealed class be placed in different files or packages? How does this affect type safety?"
Answer: No, all direct subclasses of a sealed class must be defined in the same file. This is a compiler control for type safety guarantees. If subclasses are placed in other package files, the compiler will throw an error.
Story
When designing the business logic for payments, we added options for operation results but forgot to update the when expression. But thanks to the sealed class, the compiler highlighted unhandled cases. In another Java project, this scenario would have led to an uninitialized status being sent to production.
Story
In a project after migrating part of the data from sealed classes to regular open classes, exhaustive when checks stopped working — new statuses began to "get lost" in the handling logic, causing incorrect behavior of the interface.
Story
In an e-commerce system, there was an attempt to optimize the architecture by moving subclasses of sealed classes into separate files (each in its own module for reuse). This broke the compilation and caused emergency refactoring before the release.