In Java, what are the trade-offs of implementing a State machine with an enum versus separate state classes (or a sealed interface)?
answer
- enum constants can override abstract methods = a state machine in one file
- enum: fixed, type-safe, singletons, no per-instance data
- classes: per-state data + rich behavior, but boilerplate and open set
- sealed interface = closed set (exhaustive switch) + per-state data
- Watch transition placement + safe publication of the current-state ref
basics
~20 sJava enums can give each constant its own method body, making a compact, type-safe state machine with no extra files — great for a fixed set of simple states. Separate classes or a sealed interface scale better when states hold their own data or have rich behavior.
solid answer
~50 sJava gives you three common ways to implement State. An enum where each constant overrides abstract methods is the most compact: states are a fixed, type-safe set, singletons by construction, easy to switch over exhaustively, and need no extra files — ideal when states carry no per-instance data and behavior is modest. Separate state classes behind an interface are more flexible: a state can hold its own fields, be created with constructor arguments, and grow complex behavior, but you pay in boilerplate and lose enum's built-in exhaustiveness. A sealed interface with record/class implementations is the modern middle ground: it keeps the closed set (so switch expressions are exhaustive without a default) while allowing per-state data and pattern matching. Rule of thumb: enum for small fixed machines with stateless states; sealed interface/classes when states need data, vary widely, or the machine is large. Either way, watch transition placement and thread-safety when the current-state reference is mutated.
code
java · 19 lines// Sealed interface: closed set (exhaustive switch) + per-state data
sealed interface Conn permits Open, Closed {}
record Open(long sessionId) implements Conn {}
record Closed() implements Conn {}
static String describe(Conn c) {
return switch (c) { // no default needed: set is closed
case Open o -> "open, session " + o.sessionId();
case Closed __ -> "closed";
};
}
// Enum machine: compact, fixed, stateless states
enum Light {
RED { Light next() { return GREEN; } },
GREEN { Light next() { return YELLOW; } },
YELLOW { Light next() { return RED; } };
abstract Light next();
}go deeper
Knows enums can have methods and that classes are the 'classic' way; may not weigh the trade-offs.
Can implement a state machine both as an enum and as classes and explain when per-state data forces classes.
Compares all three encodings, cites exhaustiveness of sealed/enum, per-state data, and addresses transition placement and thread-safety.
Chooses an encoding from constraints (evolution, persistence, concurrency, performance), and knows when to drop the pattern for a table/config-driven FSM.
## Setup The State pattern needs: a **State** abstraction, one implementation per **concrete state**, and a **context** holding the current state. Java offers three idiomatic encodings, each with different ergonomics. To choose well you need to know what each gives and costs. ## Option 1 — `enum` with per-constant method bodies A Java `enum` is more than a list of names: each constant can **override abstract methods** with a constant-specific body. That lets an enum *be* a state machine. ```java enum Light { RED { Light next() { return GREEN; } }, GREEN { Light next() { return YELLOW; } }, YELLOW { Light next() { return RED; } }; abstract Light next(); } ``` **Pros** - **Compact** — the whole machine lives in one file, no extra classes. - **Type-safe & closed** — the set of states is fixed and known at compile time; a `switch` over the enum can be checked for exhaustiveness, and you can't invent an illegal state value. - **Singletons for free** — each constant is a single shared instance, so states with no per-instance data needn't be re-allocated. - **Serialization / `valueOf` / `name()`** built in, handy for persisting the current state. **Cons** - **No per-instance state** — every `RED` is the same object; an enum constant can't carry instance-specific data (e.g. a timestamp unique to *this* occurrence of the state). - **No constructor arguments per use-site** — you can't parameterize a state when you transition into it. - **Awkward when behavior is large** — big method bodies inside enum constants get unwieldy. Use it for: small, **fixed** machines whose states are essentially **stateless** strategies of behavior (traffic light, simple workflow, parser modes). ## Option 2 — separate classes behind an interface The classic GoF encoding: `interface State { ... }` with `class OpenState`, `class ClosedState`, etc. **Pros** - **Per-state data** — each state object can hold fields and be constructed with arguments (`new Retrying(attempt, backoff)`). - **Rich behavior** — large, complex per-state logic lives in its own class with its own helpers. - **Extensible** — add a state by adding a class (Open/Closed principle), no central edit. **Cons** - **Boilerplate** — many small files. - **Open set** — the compiler doesn't know all implementations, so `switch`/`instanceof` chains need a default and aren't exhaustiveness-checked. - **Allocation** — you typically `new` states on transition (usually negligible, but real for hot paths). Use it for: large machines, states that carry data, or behavior too big for enum constants. ## Option 3 — `sealed interface` + record/class implementations (modern Java) A **sealed** interface (Java 17+) declares a *closed* set of permitted implementations. It combines the best of both: ```java sealed interface Conn permits Open, Closed {} record Open(long sessionId) implements Conn {} record Closed() implements Conn {} ``` **Pros** - **Closed set like an enum** → `switch` expressions are **exhaustive without a default**, and the compiler errors if you add a state and forget a branch. - **Per-state data like classes** → records can carry fields (`Open` holds a `sessionId`). - **Pattern matching** — `switch (conn) { case Open o -> ...; case Closed c -> ...; }` reads cleanly and binds the data. **Cons** - Requires modern Java (17+ for sealed; later for full pattern-matching ergonomics). - Still more verbose than a tight enum for the truly trivial case. Use it for: machines that want both exhaustiveness *and* per-state data — often the best default in current Java. ## Cross-cutting concerns (all three) - **Transition placement** — decide whether the state or the context owns `next`/`setState`. Enums often return the next constant; class/sealed encodings often `return new NextState(...)`. - **Thread-safety** — if the context's current-state field is mutated concurrently, publish it safely (e.g. `volatile`, a lock, or an immutable swap via `AtomicReference`). Immutable states (enums, records) make this much easier because the state objects themselves can't be corrupted; only the reference needs safe publication. - **Persistence** — enums serialize by `name()` trivially; class/sealed states need an explicit mapping to/from a stored discriminator. ## Decision summary - Few, fixed, **stateless** states, modest behavior → **enum**. - States need **their own data** or **large** behavior, set may grow → **classes**. - Want **exhaustiveness + per-state data + pattern matching** → **sealed interface** (modern default).
- Why can a sealed interface give exhaustive switch expressions when a plain interface cannot?A sealed interface declares the complete, fixed list of permitted implementations at compile time, so the compiler knows every possible case and can verify a switch covers them all (and reject it if you add a new permitted type without handling it). A plain interface is open — any class anywhere could implement it — so the compiler can't prove exhaustiveness and requires a default branch.
- When would an enum-based state machine become a poor choice?When states need per-instance data (each occurrence carrying its own values), when you must construct a state with arguments on transition, or when per-state behavior is large and complex. Enum constants are singletons with fixed identity, so they can't hold instance-specific data — at that point switch to classes or a sealed interface.
saying these in an interview costs you the question
- Claiming Java enums can't hold behavior — each constant can override abstract methods.
- Thinking a plain (non-sealed) interface gives exhaustive switches — it doesn't; only sealed/enum do.
- Assuming enum constants can carry per-instance data — they are singletons.
- Ignoring safe publication of the mutable current-state reference under concurrency.