A Finite State Machine (FSM) models a system as a set of discrete states and the transitions between them. In software engineering, this pattern prevents "illegal states" (e.g., a cancelled order being shipped) by making transitions explicit and type-safe.
PENDING, ACTIVE).Modern Java allows for extremely robust FSMs using sealed interfaces and records. This ensures that the set of states is fixed and exhaustive, allowing the compiler to check for missing transition logic.
public sealed interface ConnectionState
permits Disconnected, Connecting, Connected, Error {}
public record Disconnected() implements ConnectionState {}
public record Connecting(Instant attemptStarted) implements ConnectionState {}
public record Connected(String sessionId, Instant connectedAt) implements ConnectionState {}
public record Error(String message, int retryCount) implements ConnectionState {}
public class Connection {
private ConnectionState state = new Disconnected();
public void connect() {
state = switch (state) {
case Disconnected d -> new Connecting(Instant.now());
case Error e when e.retryCount() < 3 -> new Connecting(Instant.now());
case Connecting c -> c; // Idempotent
case Connected c -> throw new IllegalStateException("Already connected");
default -> throw new IllegalStateException("Cannot connect from " + state);
};
}
public void onEstablished(String sid) {
if (state instanceof Connecting) {
this.state = new Connected(sid, Instant.now());
} else {
throw new IllegalStateException("Received SID while in state: " + state);
}
}
}
For complex machines with many states, a transition table (often implemented as a Map<State, Map<Event, State>>) is cleaner than switch blocks.
| From State | Event | To State | Action/Guard |
|---|---|---|---|
INITIAL | START | RUNNING | initSystem() |
RUNNING | PAUSE | PAUSED | persistContext() |
PAUSED | RESUME | RUNNING | loadContext() |
RUNNING | ERROR | FAILED | logError() |
TransitionManager or the State object itself should handle the logic.isCancelled, isShipped, isDelivered instead of an enum or sealed type. This allows shipped && cancelled == true, which is an impossible state.trackingNumber is not null, it's shipped") instead of an explicit state indicator.