Choosing Between Data Classes and Sealed Classes for State Management
Learn when to use Kotlin data classes versus sealed classes for state management. This guide compares product types and sum types to help you build type-safe application architectures.
20 May 2026, 11:43 UTC

The State Modeling Dilemma
When designing the state of a Kotlin application—such as a UI state or a network response handler—developers often struggle to choose between data class and sealed class. Choosing the wrong one leads to either repetitive boilerplate code or a lack of type safety that causes runtime crashes when new states are added.
The core decision rests on whether your state is a Product Type (a combination of properties) or a Sum Type (one of several distinct possibilities).
Comparison: Data Classes vs. Sealed Classes
| Feature | Data Class | Sealed Class/Interface |
|---|---|---|
| Primary Purpose | Holding immutable values (Value Objects) | Defining restricted hierarchies (State Machines) |
| Compiler Check | None (standard object behavior) | Exhaustive when expression checks |
| Generated Methods | equals(), hashCode(), toString(), copy() |
None (inherited from Any) |
| Inheritance | Cannot be open or abstract |
Designed for inheritance (restricted subclasses) |
Trade-offs and Decision Logic
Use Data Classes for Value Objects
Use a data class when the identity of the object is defined entirely by its data. If two objects with the same property values should be considered identical, a data class is the correct choice. This is critical for state updates in frameworks like Jetpack Compose or Redux, where copy() is used to trigger UI refreshes by creating a new instance with modified values.
Use Sealed Classes for State Transitions
Use a sealed class when a variable can only be one of a fixed set of types. For example, a network request cannot be simultaneously "Loading" and "Success." Sealed classes allow the compiler to enforce that every possible state is handled in a when block, eliminating the need for a generic else branch that often hides bugs.
The Hybrid Approach: Sealed Hierarchies
In practical engineering, these are rarely used in isolation. The most robust pattern is to define a sealed class or sealed interface as the base, with data class or data object as the specific implementations. This provides both the exhaustive type checking of sealed classes and the value-equality of data classes.
Concrete Implementation: UI State Machine
The following example demonstrates a state manager for a user profile screen. It assumes Kotlin 1.5+ for the use of sealed interface, which allows for more flexible inheritance than sealed classes.
// Define the possible states of the screen
sealed interface ProfileState {
data object Loading : ProfileState
data class Success(
val username: String,
val email: String
) : ProfileState
data class Error(
val message: String,
val code: Int
) : ProfileState
}
fun render(state: ProfileState) {
// The compiler forces us to handle Loading, Success, and Error
when (state) {
is ProfileState.Loading -> println("Showing spinner...")
is ProfileState.Success -> println("Welcome, ${state.username}")
is ProfileState.Error -> println("Error ${state.code}: ${state.message}")
}
}
Limitations and Risks
- Package Visibility: If your sealed class hierarchy is split across multiple files, all subclasses must reside in the same package.
- Mutable State: Avoid using
varinside data classes. Using mutable properties breaks the reliability of the generatedhashCode(), which can lead to objects "disappearing" fromHashSetorHashMapcollections.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.