State
Lets an object alter its behavior when its internal state changes — as if the object changed class at runtime. Each possible state is encapsulated in its own class, and the transitions are part of the design.
Intent
Encapsulate each possible state of an object in its own class and delegate state-dependent behavior to the current state object. From the context's point of view, the object seems to "change class" when it changes state — but in reality it swaps the state object that holds the behavior.
Cataloged by the GoF (1994) as a behavioral pattern, State is the
elegant solution for finite state machines: it replaces long
conditionals (switch or if/else) that check
the current state with a structure where each state knows its own
valid behaviors and its allowed transitions.
Problem
Consider the workflow of an editorial document: draft, in review, and published. Each state allows different actions: a draft can be sent for review, but not published directly; a document in review can be approved (moves to published) or rejected (goes back to draft); a published document can't go backwards.
The naive approach encodes all these rules in conditionals inside the context:
// Naive approach — DON'T do this:
class Document {
private state: string = "draft";
publish(): void {
if (this.state === "draft") {
throw new Error("A draft cannot be published directly.");
} else if (this.state === "review") {
this.state = "published";
console.log("Document published.");
} else if (this.state === "published") {
throw new Error("Already published.");
}
}
sendForReview(): void {
if (this.state === "draft") {
this.state = "review";
} else {
throw new Error("Only drafts can go to review.");
}
}
// Every new state or action grows all the if/else above.
}
Adding a new state (e.g.: archived) requires opening every method and adding one more branch — a violation of the Open/Closed Principle. State solves this by extracting each state into its own class.
Solution
State organizes the code into three participants:
-
Context: keeps a reference to the current state
(
DocumentState) and delegates operations to it. It exposes an internal method so states can request a state change. - State (interface): declares the methods that represent state-dependent operations. All concrete states implement this interface.
- ConcreteState: implements the behavior corresponding to a specific state of the context and can trigger transitions by calling the state-change method on the context.
Key distinction: State vs Strategy
State and Strategy are structurally identical — both have a context that delegates to an interchangeable object implementing a common interface. The difference lies in intent and who controls the transitions:
- Strategy: the client chooses and injects the algorithm. The context doesn't know which strategy is active and doesn't care — there are no automatic transitions. Algorithms don't know each other.
- State: transitions are part of the design and happen automatically. States know each other: a state knows which other states it can transition to. The client rarely swaps the state directly — the current state does that in response to events.
Structure
«interface»
DocumentState
┌────────────────────────────────────────┐
│ + publish(ctx: Document): void │
│ + sendForReview(ctx): void │
│ + reject(ctx: Document): void │
└────────────────────────────────────────┘
▲
┌──────────┼────────────────────────┐
│ │ │
DraftState ReviewState PublishedState
(Concrete) (Concrete) (Concrete)
send OK publish OK all: error
publish err reject OK
Document (Context)
┌────────────────────────────────────────┐
│ - state: DocumentState │
│ + publish(): void │
│ + sendForReview(): void │
│ + reject(): void │
│ + transitionTo(s): void [pkg] │
└────────────────────────────────────────┘
│ delegates to
▼
DocumentState (current)
Transition flow:
doc.sendForReview()
→ DraftState.sendForReview(doc)
→ doc.transitionTo(new ReviewState())
doc.publish()
→ ReviewState.publish(doc)
→ doc.transitionTo(new PublishedState())
Code examples
Example 1 — Document workflow (draft → review → published)
A complete implementation with a state interface, concrete states that control their own transitions, and a context that delegates without conditionals.
// ── State interface ───────────────────────────────────────────
interface DocumentState {
publish(doc: Document): void;
sendForReview(doc: Document): void;
reject(doc: Document): void;
name(): string;
}
// ── Context ───────────────────────────────────────────────────
class Document {
private state: DocumentState;
constructor(private readonly title: string) {
this.state = new DraftState();
}
// Called by concrete states — not by the client directly.
transitionTo(newState: DocumentState): void {
console.log(` [${this.title}] ${this.state.name()} -> ${newState.name()}`);
this.state = newState;
}
publish(): void { this.state.publish(this); }
sendForReview(): void { this.state.sendForReview(this); }
reject(): void { this.state.reject(this); }
getStateName(): string { return this.state.name(); }
}
// ── ConcreteStates ────────────────────────────────────────────
class DraftState implements DocumentState {
publish(doc: Document): void {
throw new Error("A draft cannot be published without review.");
}
sendForReview(doc: Document): void {
doc.transitionTo(new ReviewState());
}
reject(doc: Document): void {
throw new Error("A draft cannot be rejected.");
}
name(): string { return "Draft"; }
}
class ReviewState implements DocumentState {
publish(doc: Document): void {
doc.transitionTo(new PublishedState());
}
sendForReview(doc: Document): void {
throw new Error("Already in review.");
}
reject(doc: Document): void {
doc.transitionTo(new DraftState());
}
name(): string { return "In Review"; }
}
class PublishedState implements DocumentState {
publish(doc: Document): void {
throw new Error("Already published.");
}
sendForReview(doc: Document): void {
throw new Error("A published document cannot go back to review.");
}
reject(doc: Document): void {
throw new Error("A published document cannot be rejected.");
}
name(): string { return "Published"; }
}
// ── Usage ────────────────────────────────────────────────────
const doc = new Document("Design Patterns Guide");
doc.sendForReview();
// [Design Patterns Guide] Draft -> In Review
doc.reject();
// [Design Patterns Guide] In Review -> Draft
doc.sendForReview();
// [Design Patterns Guide] Draft -> In Review
doc.publish();
// [Design Patterns Guide] In Review -> Published
console.log("Final state:", doc.getStateName());
// Final state: Published
try {
doc.publish(); // already published
} catch (e) {
console.log("Expected error:", (e as Error).message);
// Expected error: Already published.
}
<?php
// ── State interface ───────────────────────────────────────────
interface DocumentState
{
public function publish(Document $doc): void;
public function sendForReview(Document $doc): void;
public function reject(Document $doc): void;
public function name(): string;
}
// ── Context ───────────────────────────────────────────────────
class Document
{
private DocumentState $state;
public function __construct(private readonly string $title)
{
$this->state = new DraftState();
}
public function transitionTo(DocumentState $newState): void
{
printf(" [%s] %s -> %s\n", $this->title, $this->state->name(), $newState->name());
$this->state = $newState;
}
public function publish(): void { $this->state->publish($this); }
public function sendForReview(): void { $this->state->sendForReview($this); }
public function reject(): void { $this->state->reject($this); }
public function getStateName(): string { return $this->state->name(); }
}
// ── ConcreteStates ────────────────────────────────────────────
class DraftState implements DocumentState
{
public function publish(Document $doc): void
{
throw new \LogicException('A draft cannot be published without review.');
}
public function sendForReview(Document $doc): void
{
$doc->transitionTo(new ReviewState());
}
public function reject(Document $doc): void
{
throw new \LogicException('A draft cannot be rejected.');
}
public function name(): string { return 'Draft'; }
}
class ReviewState implements DocumentState
{
public function publish(Document $doc): void
{
$doc->transitionTo(new PublishedState());
}
public function sendForReview(Document $doc): void
{
throw new \LogicException('Already in review.');
}
public function reject(Document $doc): void
{
$doc->transitionTo(new DraftState());
}
public function name(): string { return 'In Review'; }
}
class PublishedState implements DocumentState
{
public function publish(Document $doc): void
{
throw new \LogicException('Already published.');
}
public function sendForReview(Document $doc): void
{
throw new \LogicException('A published document cannot go back to review.');
}
public function reject(Document $doc): void
{
throw new \LogicException('A published document cannot be rejected.');
}
public function name(): string { return 'Published'; }
}
// ── Usage ────────────────────────────────────────────────────
$doc = new Document('Design Patterns Guide');
$doc->sendForReview();
// [Design Patterns Guide] Draft -> In Review
$doc->reject();
// [Design Patterns Guide] In Review -> Draft
$doc->sendForReview();
// [Design Patterns Guide] Draft -> In Review
$doc->publish();
// [Design Patterns Guide] In Review -> Published
echo "Final state: " . $doc->getStateName() . "\n";
// Final state: Published
try {
$doc->publish();
} catch (\LogicException $e) {
echo "Expected error: " . $e->getMessage() . "\n";
// Expected error: Already published.
}
Example 2 — Singleton state: shared state objects
When states have no data of their own (they're stateless), they can be shared as singletons — saving allocations. This is an optimization mentioned by the GoF and illustrates the relationship between State and Singleton.
// Stateless states can be shared — a single instance per type.
// Here we use typed object literals (lighter than classes in TS for simple cases).
interface PlayerState {
play(player: MediaPlayer): void;
pause(player: MediaPlayer): void;
stop(player: MediaPlayer): void;
name(): string;
}
// Singleton state as a typed object literal.
const Stopped: PlayerState = {
play(player) { player.transitionTo(Playing); },
pause(_p) { /* ignore — already stopped */ },
stop(_p) { /* already stopped */ },
name() { return "Stopped"; },
};
const Playing: PlayerState = {
play(_p) { /* already playing */ },
pause(player){ player.transitionTo(Paused); },
stop(player) { player.transitionTo(Stopped); },
name() { return "Playing"; },
};
const Paused: PlayerState = {
play(player) { player.transitionTo(Playing); },
pause(_p) { /* already paused */ },
stop(player) { player.transitionTo(Stopped); },
name() { return "Paused"; },
};
class MediaPlayer {
private state: PlayerState = Stopped;
transitionTo(newState: PlayerState): void {
console.log(` ${this.state.name()} -> ${newState.name()}`);
this.state = newState;
}
play(): void { this.state.play(this); }
pause(): void { this.state.pause(this); }
stop(): void { this.state.stop(this); }
getState(): string { return this.state.name(); }
}
// ── Usage ────────────────────────────────────────────────────
const player = new MediaPlayer();
console.log("Start:", player.getState()); // Start: Stopped
player.play(); // Stopped -> Playing
player.pause(); // Playing -> Paused
player.play(); // Paused -> Playing
player.stop(); // Playing -> Stopped
console.log("End:", player.getState()); // End: Stopped
<?php
// Stateless states in PHP — classes with a single instance (lightweight flyweight/singleton).
interface PlayerState
{
public function play(MediaPlayer $player): void;
public function pause(MediaPlayer $player): void;
public function stop(MediaPlayer $player): void;
public function name(): string;
}
class StoppedState implements PlayerState
{
private static ?self $instance = null;
private function __construct() {}
public static function getInstance(): self
{
return self::$instance ??= new self();
}
public function play(MediaPlayer $player): void { $player->transitionTo(PlayingState::getInstance()); }
public function pause(MediaPlayer $player): void { /* already stopped — ignore */ }
public function stop(MediaPlayer $player): void { /* already stopped */ }
public function name(): string { return 'Stopped'; }
}
class PlayingState implements PlayerState
{
private static ?self $instance = null;
private function __construct() {}
public static function getInstance(): self
{
return self::$instance ??= new self();
}
public function play(MediaPlayer $player): void { /* already playing */ }
public function pause(MediaPlayer $player): void { $player->transitionTo(PausedState::getInstance()); }
public function stop(MediaPlayer $player): void { $player->transitionTo(StoppedState::getInstance()); }
public function name(): string { return 'Playing'; }
}
class PausedState implements PlayerState
{
private static ?self $instance = null;
private function __construct() {}
public static function getInstance(): self
{
return self::$instance ??= new self();
}
public function play(MediaPlayer $player): void { $player->transitionTo(PlayingState::getInstance()); }
public function pause(MediaPlayer $player): void { /* already paused */ }
public function stop(MediaPlayer $player): void { $player->transitionTo(StoppedState::getInstance()); }
public function name(): string { return 'Paused'; }
}
class MediaPlayer
{
private PlayerState $state;
public function __construct()
{
$this->state = StoppedState::getInstance();
}
public function transitionTo(PlayerState $newState): void
{
printf(" %s -> %s\n", $this->state->name(), $newState->name());
$this->state = $newState;
}
public function play(): void { $this->state->play($this); }
public function pause(): void { $this->state->pause($this); }
public function stop(): void { $this->state->stop($this); }
public function getState(): string { return $this->state->name(); }
}
// ── Usage ────────────────────────────────────────────────────
$player = new MediaPlayer();
echo "Start: " . $player->getState() . "\n"; // Start: Stopped
$player->play(); // Stopped -> Playing
$player->pause(); // Playing -> Paused
$player->play(); // Paused -> Playing
$player->stop(); // Playing -> Stopped
echo "End: " . $player->getState() . "\n"; // End: Stopped
When to use
- When an object's behavior radically changes depending on its state and there are many possible states: workflows (order, document, ticket), media players, network connections (connected/disconnected/reconnecting), communication ports.
- When state transitions are complex and rule-driven: each state knows which other states it can come from and go to. This becomes clear and testable when encapsulated in state classes.
-
To eliminate state conditionals scattered across several
methods: if you have
if (this.state === "x")repeated in dozens of methods, State extracts that logic to where it belongs — inside each state.
When to avoid
- When there are only two or three simple, stable states: an enum with a direct conditional is more readable than building the entire State class hierarchy for a trivial machine.
- When behavior doesn't change per state: if every state runs the same methods with the same logic, the pattern adds no value — you're creating empty classes.
Pros and cons
Pros
- Eliminates large state conditionals scattered across the context's code.
- Each state is an isolated class — easy to test, understand, and modify without affecting other states.
- New states can be added without changing the context or existing states (Open/Closed Principle).
- Explicit transitions inside each state make the flow readable and auditable.
- Stateless states can be shared as singletons (memory optimization).
Cons
- Class explosion for state machines with many states — each state becomes a class.
- The global view of transitions gets spread across state classes — harder to see the whole diagram in one place.
- For simple machines, the class overhead is unnecessary — a transition table or an enum may be enough.
Common pitfalls
1. Where to put the transition logic
The trickiest decision: who triggers the state transition — the Context or the ConcreteState? The GoF recommends that the states themselves trigger transitions, since they know the rules of when and where to go. This distributes transition knowledge to each state, making each one independent. The risk: states that reference each other mutually create circular coupling. To avoid that, pass the Context to the state (as shown in the examples) instead of instantiating states directly inside other states when possible.
2. Class explosion for simple machines
Rule of thumb: if the machine has fewer than four
states and the transitions are simple, consider a lighter approach: a
state enum plus a transition method with a centralized
switch in the Context. Reserve the full State pattern for
machines where per-state behavior is substantially different and
transitions have meaningful logic.
3. States with data vs stateless states
If a state needs to store its own data (e.g.: a retry counter in a "reconnecting" state), it can't be shared as a singleton — each Context instance needs its own state instance. Stateless states are the exception, not the rule. Try to keep the context's data in the Context and leave states as pure behavior and transition logic.
4. Confusing State with Strategy
Structurally identical, but the difference in intent matters in practice. If you're modeling automatic transitions and the "objects" know each other to transition between one another — it's State. If the client chooses and injects the behavior externally and there are no transitions between "states" — it's Strategy. The diagnostic question: "who decides the next variation — the object itself or the external client?". If the object itself decides, it's State.
Related patterns
State interacts with other behavioral and creational patterns:
Strategy is State's "structural sibling": both encapsulate variable behavior in interchangeable objects, but Strategy is controlled externally by the client (no automatic transitions) while State controls its own transitions. Singleton is used as an optimization when states are stateless — each concrete state without its own data can be shared as a single instance across all contexts. Observer can be combined with State to notify external components about state transitions — the Context acts as a Subject and notifies Observers when the state changes.