skip to content

When would you model behavior with an enum implementing an interface versus a sealed interface/class hierarchy? What trade-offs drive the choice?

level: seniorimportance: should knowfreq 30%

answer

  1. Enum = fixed singletons with behavior
  2. Sealed = variants with their own fields/instances
  3. Ask: do variants carry distinct data?
  4. Enum gets entries/valueOf/EnumSet/EnumMap
  5. Both give exhaustive when

basics

~10 s

Use 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 s

An 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 lines
kotlin
interface 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

for a junior

Can use an enum with an interface but may default to enums even when variants need data.

for a middle

Picks correctly based on whether variants carry data and knows both give exhaustive when.

for a senior

Articulates singleton identity, EnumSet/EnumMap, serialization, and mixing both under a shared interface.

for a principal

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

context