When would you model behavior with an enum implementing an interface versus a sealed interface/class hierarchy? What trade-offs drive the choice?
answer
- Enum = fixed singletons with behavior
- Sealed = variants with their own fields/instances
- Ask: do variants carry distinct data?
- Enum gets entries/valueOf/EnumSet/EnumMap
- Both give exhaustive when
basics
~10 sUse an enum-implementing-interface when you have a small fixed list of stateless named values that each carry behavior. Use a sealed hierarchy when variants need their own data fields or many instances.
solid answer
~50 sAn enum implementing an interface gives a **fixed, closed set of singletons**, each potentially with its own behavior via per-constant bodies, plus `entries`, `valueOf`, `ordinal`, exhaustive `when`, and zero-arg singleton identity. It's ideal for a small enumeration of stateless strategies (operations, states, directions). A **sealed interface/class** also gives exhaustiveness and a closed type set, but each subtype can be a full class with **its own constructor parameters, distinct fields, and multiple instances** — better when variants carry differing data (e.g. `Loading`, `Success(data)`, `Error(cause)`). Enums can't parameterize per *instance* beyond shared constructor args fixed at declaration; sealed types can. Trade-offs: enums are more compact, serialize trivially by name, and integrate with `EnumSet`/`EnumMap`; sealed types model heterogeneous data and recursive structures. Both interoperate with interfaces, so you can mix: a sealed interface whose leaves include enum constants. Choose enum for 'a few named constants with behavior', sealed for 'a few shapes of data with behavior'.
code
kotlin · 9 linesinterface Safe { fun isSafe(): Boolean }
enum class HttpMethod : Safe {
GET { override fun isSafe() = true },
POST { override fun isSafe() = false };
}
sealed interface Result<out T> : Safe
data class Success<T>(val value: T) : Result<T> { override fun isSafe() = true }
data class Failure(val e: Throwable) : Result<Nothing> { override fun isSafe() = false }go deeper
Can use an enum with an interface but may default to enums even when variants need data.
Picks correctly based on whether variants carry data and knows both give exhaustive when.
Articulates singleton identity, EnumSet/EnumMap, serialization, and mixing both under a shared interface.
Drives API/domain design choices, anticipating evolution, serialization strategy, and decoupling consumers via the shared interface.
## The two tools - **Enum implementing an interface:** a **closed set of singleton constants**. Each constant can override interface members (per-constant body). You get `entries`, `valueOf(name)`, `ordinal`, built-in `Comparable`, exhaustive `when`, and `EnumSet`/`EnumMap`. - **Sealed interface / sealed class:** a **closed set of subtypes** known at compile time. Each subtype is a full class/object that can declare its own constructor parameters and fields, and you can create **many instances** of a non-object subtype. ## Decision axis: do variants carry their own data? ```kotlin // Enum: fixed, stateless-ish named values with behavior enum class HttpMethod : Safe { GET { override fun isSafe() = true }, POST { override fun isSafe() = false }; } // Sealed: variants with their own distinct data sealed interface Result<out T> data class Success<T>(val value: T) : Result<T> data class Failure(val error: Throwable) : Result<Nothing> object Loading : Result<Nothing> ``` - If each variant is essentially a **named tag with behavior** and no per-instance data -> **enum + interface**. - If variants need **different fields / multiple instances / generics** -> **sealed**. ## Other trade-offs - **Identity & sets:** enum constants are singletons; `EnumSet`/`EnumMap` are highly efficient. Sealed `object`s are singletons too, but `class` leaves are not. - **Serialization:** enums round-trip by `name` trivially; sealed types need polymorphic serialization config (e.g. kotlinx.serialization `@Serializable` + sealed support). - **Exhaustiveness:** both give exhaustive `when` (no `else` needed). Adding a variant breaks `when` at compile time for both — a feature. - **Mixing:** a sealed interface can be implemented by enum constants and classes alike; an interface can be shared by both an enum and sealed leaves, letting callers depend on the interface. - **Evolution:** enums are easy to add to but every constant must satisfy abstract members; sealed types localize change to new files/classes. ## Rule of thumb 'A few named constants, each with behavior, no per-instance state' -> **enum implementing an interface**. 'A few shapes of data, each with behavior and its own fields' -> **sealed**. When unsure, ask whether two instances of the same variant could differ — if yes, you need sealed.
- Can an enum constant hold per-instance mutable state to mimic a sealed data class?It can have shared constructor args fixed at declaration, but every reference to a constant is the same singleton, so you can't have two GETs with different data — that's exactly when you need sealed.
- Do both enums and sealed types give exhaustive `when`?Yes; in both, a `when` covering all variants needs no `else`, and adding a variant turns missing branches into a compile error.
Enum constants are like the fixed buttons on a calculator; sealed subtypes are like different paper forms, each with its own blanks to fill in.
saying these in an interview costs you the question
- Reaching for an enum when variants clearly need distinct fields
- Claiming enums can have multiple distinct instances per constant
- Saying sealed types don't support exhaustive when
- Ignoring EnumSet/EnumMap and serialization differences
- Not realizing both can share a common interface