Complete Technical Guide: Sealed Classes, Sealed Interfaces & Enums in Kotlin
Table of Contents
Introduction
Enums: The Foundation
Sealed Classes: Controlled Inheritance
Sealed Interfaces: Contract Control
Comparison & Use Cases
Memory Internals
Best Practices
Introduction
Kotlin provides three powerful mechanisms for creating restricted type hierarchies: Enums, Sealed Classes, and Sealed Interfaces. Each serves different purposes in domain modeling and type safety.
Enums: The Foundation
What Are Enums?
Enums represent a fixed set of constants. Each constant is a singleton instance that can have properties and methods.
Basic Enum Example
enum class PaymentStatus {
PENDING,
PROCESSING,
COMPLETED,
FAILED,
REFUNDED
}
// Usage
fun main() {
val status = PaymentStatus.COMPLETED
println(status) // Output: COMPLETED
println(status.ordinal) // Output: 2
println(status.name) // Output: COMPLETED
}
Advanced Enum with State and Behavior
enum class PaymentMethod(
val displayName: String,
val transactionFeePercent: Double,
val requiresOnlineConnection: Boolean
) {
CREDIT_CARD(”Credit Card”, 2.9, true) {
override fun processPayment(amount: Double): Boolean {
println(”Processing credit card payment: $$amount”)
return amount > 0 && amount <= 10000
}
override fun validateCardNumber(cardNumber: String): Boolean {
return cardNumber.length == 16 && cardNumber.all { it.isDigit() }
}
},
DEBIT_CARD(”Debit Card”, 1.5, true) {
override fun processPayment(amount: Double): Boolean {
println(”Processing debit card payment: $$amount”)
return amount > 0 && amount <= 5000
}
override fun validateCardNumber(cardNumber: String): Boolean {
return cardNumber.length == 16 && cardNumber.all { it.isDigit() }
}
},
PAYPAL(”PayPal”, 3.5, true) {
override fun processPayment(amount: Double): Boolean {
println(”Processing PayPal payment: $$amount”)
return amount > 0
}
override fun validateCardNumber(cardNumber: String): Boolean = true
},
CASH(”Cash”, 0.0, false) {
override fun processPayment(amount: Double): Boolean {
println(”Processing cash payment: $$amount”)
return amount > 0 && amount <= 1000
}
override fun validateCardNumber(cardNumber: String): Boolean = true
};
abstract fun processPayment(amount: Double): Boolean
abstract fun validateCardNumber(cardNumber: String): Boolean
fun calculateFee(amount: Double): Double {
return amount * (transactionFeePercent / 100)
}
fun isOnlinePayment(): Boolean = requiresOnlineConnection
}
Enum Usage Example
fun main() {
val method = PaymentMethod.CREDIT_CARD
val amount = 1000.0
if (method.processPayment(amount)) {
val fee = method.calculateFee(amount)
println(”Payment successful! Fee: $${”%.2f”.format(fee)}”)
}
// Enum iteration
println(”\nAvailable Payment Methods:”)
PaymentMethod.values().forEach { pm ->
println(”${pm.displayName} - Fee: ${pm.transactionFeePercent}%”)
}
// Using when expression (exhaustive)
val message = when (method) {
PaymentMethod.CREDIT_CARD -> “Swipe your card”
PaymentMethod.DEBIT_CARD -> “Insert your card”
PaymentMethod.PAYPAL -> “Login to PayPal”
PaymentMethod.CASH -> “Hand over cash”
}
println(message)
}
Enum with Companion Object
enum class HttpStatus(val code: Int, val description: String) {
OK(200, “Success”),
CREATED(201, “Resource Created”),
BAD_REQUEST(400, “Bad Request”),
UNAUTHORIZED(401, “Unauthorized”),
NOT_FOUND(404, “Not Found”),
INTERNAL_ERROR(500, “Internal Server Error”);
companion object {
fun fromCode(code: Int): HttpStatus? {
return values().find { it.code == code }
}
fun isSuccess(code: Int): Boolean {
return code in 200..299
}
}
fun isError(): Boolean = code >= 400
}
// Usage
fun main() {
val status = HttpStatus.fromCode(404)
println(status?.description) // Output: Not Found
println(HttpStatus.isSuccess(200)) // true
println(HttpStatus.BAD_REQUEST.isError()) // true
}
Enum Pain Points
1. Cannot Add New Constants at Runtime
// This is IMPOSSIBLE - enums are fixed at compile time
// PaymentMethod.BITCOIN = ... // No way to do this!2. All Constants Must Share Same Structure
enum class Vehicle {
CAR, // Has wheels, engine
BOAT, // Has propeller, no wheels
PLANE // Has wings, turbines
// All must have same properties - limits flexibility
}3. Cannot Extend Enums
// This is ILLEGAL
// enum class ExtendedPaymentMethod : PaymentMethod() // Error!4. Memory Overhead with Large Number of Constants
// Each enum constant is a separate object instance
enum class LargeEnum {
VAL_1, VAL_2, VAL_3, /* ... */ VAL_10000
// 10,000 object instances created at initialization!
}5. Limited Polymorphism
Each enum constant is an instance of the same type, making complex hierarchies difficult.
Sealed Classes: Controlled Inheritance
What Are Sealed Classes?
Sealed classes allow you to define a restricted class hierarchy where all subclasses are known at compile time. Perfect for representing finite states or types.
Basic Sealed Class Example
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val code: Int) : Result<Nothing>()
data object Loading : Result<Nothing>()
data object Empty : Result<Nothing>()
}
// Usage
fun fetchUser(userId: Int): Result<User> {
return try {
val user = database.getUser(userId)
if (user != null) Result.Success(user)
else Result.Empty
} catch (e: Exception) {
Result.Error(e.message ?: “Unknown error”, 500)
}
}
fun handleResult(result: Result<User>) {
when (result) {
is Result.Success -> println(”User: ${result.data.name}”)
is Result.Error -> println(”Error ${result.code}: ${result.message}”)
Result.Loading -> println(”Loading...”)
Result.Empty -> println(”No user found”)
} // Exhaustive - compiler ensures all cases handled
}
Advanced Sealed Class: Domain Modeling
sealed class Shape {
abstract val color: String
abstract fun area(): Double
abstract fun perimeter(): Double
}
data class Circle(
override val color: String,
val radius: Double
) : Shape() {
override fun area(): Double = Math.PI * radius * radius
override fun perimeter(): Double = 2 * Math.PI * radius
}
data class Rectangle(
override val color: String,
val width: Double,
val height: Double
) : Shape() {
override fun area(): Double = width * height
override fun perimeter(): Double = 2 * (width + height)
}
data class Triangle(
override val color: String,
val base: Double,
val height: Double,
val side1: Double,
val side2: Double
) : Shape() {
override fun area(): Double = 0.5 * base * height
override fun perimeter(): Double = base + side1 + side2
}
// Complex shape with composition
data class CompositeShape(
override val color: String,
val shapes: List<Shape>
) : Shape() {
override fun area(): Double = shapes.sumOf { it.area() }
override fun perimeter(): Double = shapes.sumOf { it.perimeter() }
}
Sealed Class Hierarchy Diagram
Sealed Class for State Management
sealed class UiState<out T> {
data object Idle : UiState<Nothing>()
data object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val throwable: Throwable) : UiState<Nothing>()
fun isLoading(): Boolean = this is Loading
fun isSuccess(): Boolean = this is Success
fun getOrNull(): T? = when (this) {
is Success -> data
else -> null
}
fun getOrDefault(default: T): T = when (this) {
is Success -> data
else -> default
}
}
// Usage in ViewModel
class UserViewModel {
private val _uiState = MutableStateFlow<UiState<User>>(UiState.Idle)
val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
fun loadUser(userId: Int) {
_uiState.value = UiState.Loading
viewModelScope.launch {
try {
val user = repository.getUser(userId)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e)
}
}
}
}
// UI Layer
fun renderUi(state: UiState<User>) {
when (state) {
UiState.Idle -> showWelcomeScreen()
UiState.Loading -> showLoadingSpinner()
is UiState.Success -> showUserProfile(state.data)
is UiState.Error -> showError(state.throwable.message)
}
}
Sealed Class for Navigation
sealed class Screen(val route: String) {
data object Home : Screen(”home”)
data object Profile : Screen(”profile”)
data class UserDetail(val userId: Int) : Screen(”user/$userId”)
data class ProductDetail(val productId: String) : Screen(”product/$productId”)
data class WebView(
val url: String,
val title: String? = null
) : Screen(”webview?url=$url&title=$title”)
}
// Navigation handling
fun navigate(screen: Screen) {
when (screen) {
Screen.Home -> navController.navigate(screen.route)
Screen.Profile -> navController.navigate(screen.route)
is Screen.UserDetail -> navController.navigate(screen.route)
is Screen.ProductDetail -> navController.navigate(screen.route)
is Screen.WebView -> navController.navigate(screen.route)
}
}
Sealed Class Pain Points
1. Must Be in Same Package (Before Kotlin 1.5)
// All subclasses must be in same file or package
// Cannot extend from external libraries2. More Verbose Than Enums
// Sealed classes require more boilerplate
sealed class Status {
data object Active : Status()
data object Inactive : Status()
}
// vs Enum
enum class Status { ACTIVE, INACTIVE }3. No Ordinal or Name Properties
// Enums have built-in ordinal and name
// Sealed classes don’t - must implement manuallySealed Interfaces: Contract Control
What Are Sealed Interfaces?
Sealed interfaces (Kotlin 1.5+) restrict which types can implement them. They enable multiple inheritance while maintaining exhaustiveness.
Basic Sealed Interface Example
sealed interface ApiResponse
data class SuccessResponse(val data: String) : ApiResponse
data class ErrorResponse(val error: String, val code: Int) : ApiResponse
data object LoadingResponse : ApiResponse
fun handleResponse(response: ApiResponse) {
when (response) {
is SuccessResponse -> println(”Success: ${response.data}”)
is ErrorResponse -> println(”Error ${response.code}: ${response.error}”)
LoadingResponse -> println(”Loading...”)
} // Exhaustive!
}
Multiple Inheritance with Sealed Interfaces
sealed interface Drivable {
fun drive()
val maxSpeed: Int
}
sealed interface Flyable {
fun fly()
val maxAltitude: Int
}
sealed interface Floatable {
fun float()
val maxDepth: Int
}
// Multiple inheritance
data class FlyingCar(
override val maxSpeed: Int,
override val maxAltitude: Int
) : Drivable, Flyable {
override fun drive() = println(”Driving at $maxSpeed km/h”)
override fun fly() = println(”Flying at $maxAltitude meters”)
}
data class Submarine(
override val maxSpeed: Int,
override val maxDepth: Int
) : Drivable, Floatable {
override fun drive() = println(”Moving underwater at $maxSpeed knots”)
override fun float() = println(”Submerging to $maxDepth meters”)
}
data class AmphibiousVehicle(
override val maxSpeed: Int,
override val maxDepth: Int
) : Drivable, Floatable {
override fun drive() = println(”Driving on land at $maxSpeed km/h”)
override fun float() = println(”Swimming at depth $maxDepth meters”)
}
// Seaplane - implements all three!
data class Seaplane(
override val maxSpeed: Int,
override val maxAltitude: Int,
override val maxDepth: Int
) : Drivable, Flyable, Floatable {
override fun drive() = println(”Taxiing at $maxSpeed km/h”)
override fun fly() = println(”Flying at $maxAltitude meters”)
override fun float() = println(”Floating on water, depth capability: $maxDepth meters”)
}Sealed Interface Hierarchy Diagram
Domain Modeling with Sealed Interfaces
sealed interface PaymentProvider {
val providerName: String
fun processPayment(amount: Double): PaymentResult
}
sealed interface RefundablePayment {
fun refund(amount: Double): RefundResult
}
sealed interface RecurringPayment {
fun setupRecurring(interval: String): SubscriptionResult
}
// Implementations
data class StripePayment(
override val providerName: String = “Stripe”
) : PaymentProvider, RefundablePayment, RecurringPayment {
override fun processPayment(amount: Double) =
PaymentResult.Success(”stripe_${System.currentTimeMillis()}”)
override fun refund(amount: Double) =
RefundResult.Success(”refund_${System.currentTimeMillis()}”)
override fun setupRecurring(interval: String) =
SubscriptionResult.Active(”sub_${System.currentTimeMillis()}”)
}
data class PayPalPayment(
override val providerName: String = “PayPal”
) : PaymentProvider, RefundablePayment {
override fun processPayment(amount: Double) =
PaymentResult.Success(”paypal_${System.currentTimeMillis()}”)
override fun refund(amount: Double) =
RefundResult.Success(”refund_${System.currentTimeMillis()}”)
}
data class CashPayment(
override val providerName: String = “Cash”
) : PaymentProvider {
override fun processPayment(amount: Double) =
PaymentResult.Success(”cash_${System.currentTimeMillis()}”)
}
// Result types
sealed interface PaymentResult {
data class Success(val transactionId: String) : PaymentResult
data class Failed(val reason: String) : PaymentResult
}
sealed interface RefundResult {
data class Success(val refundId: String) : RefundResult
data class Failed(val reason: String) : RefundResult
}
sealed interface SubscriptionResult {
data class Active(val subscriptionId: String) : SubscriptionResult
data class Failed(val reason: String) : SubscriptionResult
}
Type-Safe DSL with Sealed Interfaces
sealed interface HtmlElement {
fun render(): String
}
sealed interface ContainerElement : HtmlElement {
val children: MutableList<HtmlElement>
fun addChild(element: HtmlElement) {
children.add(element)
}
}
data class Div(
val className: String? = null,
override val children: MutableList<HtmlElement> = mutableListOf()
) : ContainerElement {
override fun render(): String {
val classAttr = className?.let { “ class=\”$it\”“ } ?: “”
val content = children.joinToString(”“) { it.render() }
return “<div$classAttr>$content</div>”
}
}
data class Span(
val text: String,
val className: String? = null
) : HtmlElement {
override fun render(): String {
val classAttr = className?.let { “ class=\”$it\”“ } ?: “”
return “<span$classAttr>$text</span>”
}
}
data class Button(
val text: String,
val onClick: () -> Unit,
override val children: MutableList<HtmlElement> = mutableListOf()
) : ContainerElement {
override fun render(): String {
return “<button>${children.joinToString(”“) { it.render() }}$text</button>”
}
}
// DSL usage
fun html(init: Div.() -> Unit): Div {
val div = Div()
div.init()
return div
}
fun main() {
val page = html {
addChild(Div(className = “header”).apply {
addChild(Span(”Welcome!”, className = “title”))
})
addChild(Div(className = “content”).apply {
addChild(Span(”Hello World”))
addChild(Button(”Click Me”, onClick = { println(”Clicked!”) }))
})
}
println(page.render())
}
Sealed Interface Pain Points
1. More Abstract - Can Be Harder to Understand
// Interfaces are more abstract than classes
// May confuse beginners2. Cannot Hold State Directly
sealed interface Base {
// val state: String // Error! Interfaces can’t have state
// Must use properties in implementations
}3. Requires More Planning
// Need to think about contracts and implementations upfront
// More design overhead than sealed classesComparison & Use Cases
When to Use Each
Feature Comparison Table
Use Case Examples
Use Enum When:
// 1. Fixed set of related constants
enum class Direction { NORTH, SOUTH, EAST, WEST }
// 2. Simple state machine
enum class OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }
// 3. Configuration options
enum class LogLevel { DEBUG, INFO, WARN, ERROR }
// 4. Type-safe constants
enum class Planet(val mass: Double, val radius: Double) {
EARTH(5.97e24, 6.371e6),
MARS(6.39e23, 3.389e6)
}Use Sealed Class When:
// 1. Result/Response types
sealed class NetworkResult<T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val exception: Exception) : NetworkResult<Nothing>()
data object Loading : NetworkResult<Nothing>()
}
// 2. Complex state management
sealed class AuthState {
data object Unauthenticated : AuthState()
data class Authenticated(val user: User, val token: String) : AuthState()
data class AuthError(val message: String) : AuthState()
}
// 3. Expression/AST nodes
sealed class Expr {
data class Const(val value: Int) : Expr()
data class Add(val left: Expr, val right: Expr) : Expr()
data class Multiply(val left: Expr, val right: Expr) : Expr()
}
// 4. Domain models with variants
sealed class Notification {
data class Email(val to: String, val subject: String, val body: String) : Notification()
data class SMS(val phoneNumber: String, val message: String) : Notification()
data class Push(val deviceToken: String, val title: String, val body: String) : Notification()
}Use Sealed Interface When:
// 1. Multiple capability traits
sealed interface Serializable {
fun toJson(): String
}
sealed interface Cacheable {
val cacheKey: String
}
data class User(val id: Int, val name: String) : Serializable, Cacheable {
override fun toJson() = “”“{”id”:$id,”name”:”$name”}”“”
override val cacheKey = “user:$id”
}
// 2. Plugin/Extension systems
sealed interface Plugin {
val name: String
fun execute()
}
sealed interface ConfigurablePlugin : Plugin {
fun configure(config: Map<String, Any>)
}
// 3. Event handling with multiple sources
sealed interface AppEvent
sealed interface UserEvent : AppEvent
sealed interface SystemEvent : AppEvent
data class UserClickEvent(val x: Int, val y: Int) : UserEvent
data class SystemShutdownEvent(val reason: String) : SystemEventData Flow Comparison
Memory Internals
Enum Memory Layout
Enum Memory Characteristics:
Static Allocation: All enum constants are created at class initialization
Singleton Pattern: Each constant is a single instance (memory efficient)
Overhead per Constant: ~40-60 bytes per constant (JVM)
Object header: 12-16 bytes
namefield: 8 bytes (reference)ordinalfield: 4 bytesCustom fields: depends on definition
Padding: alignment
enum class Status {
ACTIVE, INACTIVE, PENDING
}
// Memory:
// - Status class object: ~100 bytes
// - ACTIVE instance: ~50 bytes
// - INACTIVE instance: ~50 bytes
// - PENDING instance: ~50 bytes
// - Static array: 24 bytes (reference array)
// Total: ~274 bytesSealed Class Memory Layout
Sealed Class Memory Characteristics:
Dynamic Allocation: Instances created when needed
Per-Instance Overhead: ~24-40 bytes base (JVM)
Object header: 12-16 bytes
VTable pointer: 8 bytes
Fields: depends on class definition
Polymorphism Cost: Virtual method dispatch through VTable
sealed class Result<T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
}
// Memory for Success(User(id=1, name=”John”)):
// - Object header: 16 bytes
// - VTable pointer: 8 bytes
// - data reference: 8 bytes
// - User object: 32 bytes (header + fields)
// Total per instance: ~64 bytesSealed Interface Memory Layout
Sealed Interface Memory Characteristics:
Multiple VTables: One VTable per implemented interface
Interface Overhead: ~8 bytes per interface reference
Flexible but Costly: More memory than single inheritance
sealed interface Serializable {
fun toJson(): String
}
sealed interface Cacheable {
val cacheKey: String
}
data class User(
val id: Int,
val name: String
) : Serializable, Cacheable {
override fun toJson() = “”“{”id”:$id,”name”:”$name”}”“”
override val cacheKey = “user:$id”
}
// Memory for User(1, “John”):
// - Object header: 16 bytes
// - VTable for Serializable: 8 bytes
// - VTable for Cacheable: 8 bytes
// - id field: 4 bytes
// - name reference: 8 bytes
// - String “John”: ~40 bytes
// - cacheKey computed property: cached or computed
// Total: ~84+ bytes per instanceMemory Comparison
Detailed Memory Breakdown
Real-World Memory Impact
// Scenario 1: Status tracking (10,000 objects)
enum class Status { ACTIVE, INACTIVE }
// Memory: ~250 bytes (shared singletons) ✅ Winner
sealed class Status {
object Active : Status()
object Inactive : Status()
}
// Memory: ~200 bytes (2 singleton objects)
// Scenario 2: User requests (10,000 different instances)
sealed class ApiResponse {
data class Success(val data: String) : ApiResponse()
data class Error(val message: String) : ApiResponse()
}
// Memory: ~600 KB (10,000 * ~60 bytes) ✅ Winner
enum class ApiResponse {
SUCCESS, ERROR // Can’t hold different data per call ❌
}
// Scenario 3: Plugin system (100 plugins, multiple capabilities)
sealed interface Plugin { val name: String }
sealed interface Configurable { fun configure(config: Map<String, Any>) }
sealed interface Lifecycle { fun start(); fun stop() }
data class MyPlugin(
override val name: String
) : Plugin, Configurable, Lifecycle {
// Implementation
}
// Memory: ~15 KB (100 * ~150 bytes) ✅ Winner for flexibilityBest Practices
1. Choose the Right Tool
// ❌ BAD: Using sealed class for simple constants
sealed class Color {
object Red : Color()
object Green : Color()
object Blue : Color()
}
// ✅ GOOD: Use enum for simple constants
enum class Color { RED, GREEN, BLUE }
// ❌ BAD: Using enum for complex hierarchies
enum class Response {
SUCCESS, // Can’t hold data
ERROR // Can’t hold error details
}
// ✅ GOOD: Use sealed class for complex data
sealed class Response {
data class Success(val data: String) : Response()
data class Error(val code: Int, val message: String) : Response()
}
// ❌ BAD: Single inheritance when multiple needed
sealed class Printable {
abstract fun print()
}
// Can’t also be Serializable without interface
// ✅ GOOD: Use sealed interface for multiple capabilities
sealed interface Printable { fun print() }
sealed interface Serializable { fun serialize(): String }
data class Document(val content: String) : Printable, Serializable {
override fun print() = println(content)
override fun serialize() = content
}2. Leverage Exhaustive When
// ✅ GOOD: Exhaustive when prevents bugs
sealed class Result<T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
fun handleResult(result: Result<User>) {
when (result) {
is Result.Success -> showUser(result.data)
is Result.Error -> showError(result.message)
Result.Loading -> showLoading()
} // Compiler ensures all cases handled
}
// ❌ BAD: Non-exhaustive when
fun handleResultBad(result: Result<User>) {
when (result) {
is Result.Success -> showUser(result.data)
// Missing other cases - runtime crash possible!
}
}
// ✅ GOOD: Use else for future-proofing if needed
fun handleResultSafe(result: Result<User>) {
when (result) {
is Result.Success -> showUser(result.data)
is Result.Error -> showError(result.message)
else -> showLoading() // Handles any new cases
}
}3. Use Data Classes with Sealed Classes
// ✅ GOOD: Data classes get equals, hashCode, copy, toString
sealed class NetworkEvent {
data class Connected(val timestamp: Long) : NetworkEvent()
data class Disconnected(val reason: String) : NetworkEvent()
data object Connecting : NetworkEvent()
}
// Usage benefits
fun example() {
val event1 = NetworkEvent.Connected(System.currentTimeMillis())
val event2 = event1.copy(timestamp = System.currentTimeMillis() + 1000)
println(event1) // Pretty print: Connected(timestamp=1234567890)
println(event1 == event2) // Structural equality
}
// ❌ BAD: Regular classes lose these benefits
sealed class NetworkEventBad {
class Connected(val timestamp: Long) : NetworkEventBad()
// No automatic equals, hashCode, copy, toString
}4. Design for Extension
// ✅ GOOD: Sealed for known hierarchy, open for extension
sealed interface BaseEvent
// Internal known events
data class ClickEvent(val x: Int, val y: Int) : BaseEvent
data class KeyEvent(val key: String) : BaseEvent
// But can still add more in same module
data class ScrollEvent(val delta: Int) : BaseEvent
// ❌ BAD: Overly restrictive
final class Event // Can’t extend at all
// ❌ BAD: Too open
open class Event // Anyone can extend, loses exhaustiveness5. Naming Conventions
// ✅ GOOD: Clear, descriptive names
sealed class AuthenticationState {
data object Unauthenticated : AuthenticationState()
data class Authenticated(val user: User) : AuthenticationState()
data class AuthenticationFailed(val error: String) : AuthenticationState()
}
// ✅ GOOD: Suffix for types
sealed class PaymentResult { /* ... */ }
sealed interface Cacheable { /* ... */ }
enum class HttpMethod { /* ... */ }
// ❌ BAD: Unclear names
sealed class Thing { /* What is this? */ }
sealed class A { /* Meaningless */ }6. Combine for Maximum Effect
// ✅ EXCELLENT: Enum inside sealed class
sealed class FormField {
data class Input(
val name: String,
val type: InputType,
val required: Boolean
) : FormField()
data class Select(
val name: String,
val options: List<String>
) : FormField()
enum class InputType {
TEXT, EMAIL, PASSWORD, NUMBER, DATE
}
}
// ✅ EXCELLENT: Sealed interface with enum
sealed interface Permission {
val level: AccessLevel
enum class AccessLevel { READ, WRITE, ADMIN }
}
data class FilePermission(
val path: String,
override val level: Permission.AccessLevel
) : Permission
data class DatabasePermission(
val table: String,
override val level: Permission.AccessLevel
) : Permission7. Error Handling Pattern
// ✅ BEST PRACTICE: Comprehensive error modeling
sealed class DomainError {
abstract val message: String
abstract val code: String
data class ValidationError(
override val message: String,
override val code: String = “VALIDATION_ERROR”,
val field: String
) : DomainError()
data class NotFoundError(
override val message: String,
override val code: String = “NOT_FOUND”,
val resourceId: String
) : DomainError()
data class UnauthorizedError(
override val message: String,
override val code: String = “UNAUTHORIZED”
) : DomainError()
data class ServerError(
override val message: String,
override val code: String = “SERVER_ERROR”,
val exception: Throwable? = null
) : DomainError()
}
// Usage
fun processRequest(): Either<DomainError, User> {
return when {
!isAuthenticated() ->
Either.Left(DomainError.UnauthorizedError(”User not authenticated”))
!isValid(input) ->
Either.Left(DomainError.ValidationError(”Invalid input”, field = “email”))
else ->
Either.Right(getUser())
}
}8. State Machine Pattern
// ✅ EXCELLENT: Type-safe state machine
sealed class ConnectionState {
data object Disconnected : ConnectionState() {
fun connect() = Connecting
}
data object Connecting : ConnectionState() {
fun onSuccess() = Connected(System.currentTimeMillis())
fun onFailure(reason: String) = Failed(reason)
}
data class Connected(val connectedAt: Long) : ConnectionState() {
fun disconnect() = Disconnected
}
data class Failed(val reason: String) : ConnectionState() {
fun retry() = Connecting
fun cancel() = Disconnected
}
}
// State transitions are type-safe
fun manageConnection() {
var state: ConnectionState = ConnectionState.Disconnected
state = when (state) {
ConnectionState.Disconnected -> state.connect()
ConnectionState.Connecting -> state.onSuccess()
is ConnectionState.Connected -> state.disconnect()
is ConnectionState.Failed -> state.retry()
}
}9. Performance Optimization
// ✅ GOOD: Use object for stateless subclasses
sealed class Operation {
data object Start : Operation() // Singleton
data object Stop : Operation() // Singleton
data class Configure(val config: Map<String, Any>) : Operation() // Has state
}
// ❌ BAD: Using data class for stateless types
sealed class OperationBad {
data class Start(val dummy: Unit = Unit) : OperationBad() // Wasteful!
}
// ✅ GOOD: Cache frequently used instances
sealed class Result<T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
companion object {
private val emptySuccess = Success(Unit)
fun <T> success(data: T): Success<T> = Success(data)
fun emptySuccess(): Success<Unit> = emptySuccess
}
}10. Documentation
// ✅ GOOD: Document your sealed hierarchies
/**
* Represents the possible states of a network request.
*
* @see Loading Initial state before request
* @see Success Request completed successfully
* @see Error Request failed
*/
sealed class NetworkState<out T> {
/** Request is in progress */
data object Loading : NetworkState<Nothing>()
/** Request completed successfully with data */
data class Success<T>(val data: T) : NetworkState<T>()
/** Request failed with an error message */
data class Error(
val message: String,
val code: Int? = null,
val cause: Throwable? = null
) : NetworkState<Nothing>()
}Performance Considerations
Compilation Impact
Runtime Performance
// Benchmark comparison
fun benchmarkEnum() {
val status = Status.ACTIVE
repeat(1_000_000) {
when (status) {
Status.ACTIVE -> 1
Status.INACTIVE -> 2
}
}
// Time: ~5ms
}
fun benchmarkSealedClass() {
val status: StatusSealed = StatusSealed.Active
repeat(1_000_000) {
when (status) {
StatusSealed.Active -> 1
StatusSealed.Inactive -> 2
}
}
// Time: ~8ms (virtual dispatch overhead)
}
fun benchmarkSealedInterface() {
val status: StatusInterface = StatusInterfaceImpl.Active
repeat(1_000_000) {
when (status) {
is StatusInterfaceImpl.Active -> 1
is StatusInterfaceImpl.Inactive -> 2
}
}
// Time: ~12ms (interface dispatch + type checking)
}Memory Allocation Patterns
Advanced Patterns
1. Result Type with Extensions
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: Throwable) : Result<Nothing>()
}
// Extension functions
fun <T> Result<T>.getOrNull(): T? = when (this) {
is Result.Success -> value
is Result.Failure -> null
}
fun <T> Result<T>.getOrElse(default: T): T = when (this) {
is Result.Success -> value
is Result.Failure -> default
}
fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = when (this) {
is Result.Success -> Result.Success(transform(value))
is Result.Failure -> this
}
fun <T, R> Result<T>.flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
is Result.Success -> transform(value)
is Result.Failure -> this
}
// Usage
fun processData(): Result<String> {
return fetchData()
.map { it.uppercase() }
.flatMap { validate(it) }
.map { sanitize(it) }
}
2. Event Sourcing Pattern
sealed class DomainEvent {
abstract val timestamp: Long
abstract val aggregateId: String
}
sealed class UserEvent : DomainEvent() {
data class UserCreated(
override val timestamp: Long,
override val aggregateId: String,
val email: String,
val name: String
) : UserEvent()
data class EmailChanged(
override val timestamp: Long,
override val aggregateId: String,
val newEmail: String
) : UserEvent()
data class UserDeleted(
override val timestamp: Long,
override val aggregateId: String
) : UserEvent()
}
// Event handler
class UserAggregate(val id: String) {
private var email: String? = null
private var name: String? = null
private var deleted: Boolean = false
fun apply(event: UserEvent) {
when (event) {
is UserEvent.UserCreated -> {
email = event.email
name = event.name
}
is UserEvent.EmailChanged -> {
email = event.newEmail
}
is UserEvent.UserDeleted -> {
deleted = true
}
}
}
}3. Command Pattern
sealed class Command {
abstract fun execute()
abstract fun undo()
}
data class CreateUserCommand(
val userId: String,
val email: String
) : Command() {
override fun execute() {
database.insertUser(userId, email)
}
override fun undo() {
database.deleteUser(userId)
}
}
data class UpdateEmailCommand(
val userId: String,
val newEmail: String,
private var oldEmail: String? = null
) : Command() {
override fun execute() {
oldEmail = database.getUserEmail(userId)
database.updateEmail(userId, newEmail)
}
override fun undo() {
oldEmail?.let { database.updateEmail(userId, it) }
}
}
// Command executor with history
class CommandExecutor {
private val history = mutableListOf<Command>()
fun execute(command: Command) {
command.execute()
history.add(command)
}
fun undo() {
history.removeLastOrNull()?.undo()
}
}Summary
Quick Decision Guide
Key Takeaways
Enums: Perfect for fixed constants, most memory efficient, built-in features
Sealed Classes: Best for type hierarchies with different data, exhaustive pattern matching
Sealed Interfaces: Ideal for multiple capabilities, most flexible, slight performance cost
When to Use What
Use Enum for: Status codes, directions, configuration, simple state machines
Use Sealed Class for: Result types, UI states, domain models, navigation
Use Sealed Interface for: Capabilities, traits, plugin systems, DSLs
All three mechanisms provide compile-time safety and exhaustive checks, making your Kotlin code more robust and maintainable.
















