Marcelo Pastorino, Software Developer

State Pattern

A behavioral design pattern that lets an object change its behavior when its internal state changes, by delegating to interchangeable state objects.

The state pattern is a behavioral design pattern that lets an object alter its behavior when its internal state changes, so that it appears to change its class. Each state is represented by its own object, the context delegates behavior to the current state, and the states themselves decide when to hand control to the next one.

The problem it solves

Objects with a lifecycle, such as a document that is draft, in review, or published; a media player that is stopped, playing, or paused; or a connection that is open or closed, tend to accumulate conditionals. Every method inspects a status field and branches, so the same switch appears in play(), pause(), and stop(). Adding a state means editing every method, and the transition rules are scattered and easy to break.

The state pattern moves each variant of behavior into its own class. The context holds a reference to a state object and delegates to it; the branching disappears because the current state already is the right behavior.

Structure

  • State, the interface common to all states, declaring the operations the context supports.
  • Concrete states, one class per state, each implementing the operations and triggering transitions to other states.
  • Context, the object with the lifecycle; it holds the current state and forwards requests to it.
  • The transition logic lives inside the states (or, in simpler variants, in the context), keeping it in one place per state.
Media player states and the transitions each one triggers

In code

A media player forwards play and pause to its current state. Each state responds appropriately and, when needed, moves the player to the next state. The player itself has no conditionals.

// Concrete states: each handles actions and triggers transitions
class Stopped {
  play(player) {
    console.log("Starting playback");
    player.state = new Playing();
  }
  pause(player) {
    console.log("Nothing to pause");
  }
}

class Playing {
  play(player) {
    console.log("Already playing");
  }
  pause(player) {
    console.log("Pausing");
    player.state = new Paused();
  }
}

class Paused {
  play(player) {
    console.log("Resuming");
    player.state = new Playing();
  }
  pause(player) {
    console.log("Already paused");
  }
}

// Context: delegates to its current state, which decides the next state
class Player {
  constructor() {
    this.state = new Stopped();
  }
  play() {
    this.state.play(this);
  }
  pause() {
    this.state.pause(this);
  }
}

const player = new Player();
player.play(); // Starting playback  (Stopped -> Playing)
player.pause(); // Pausing            (Playing -> Paused)
player.play(); // Resuming           (Paused -> Playing)
// State: the interface common to all states
interface PlayerState {
  play(player: Player): void;
  pause(player: Player): void;
}

// Concrete states: each handles actions and triggers transitions
class Stopped implements PlayerState {
  play(player: Player): void {
    console.log("Starting playback");
    player.state = new Playing();
  }
  pause(player: Player): void {
    console.log("Nothing to pause");
  }
}

class Playing implements PlayerState {
  play(player: Player): void {
    console.log("Already playing");
  }
  pause(player: Player): void {
    console.log("Pausing");
    player.state = new Paused();
  }
}

class Paused implements PlayerState {
  play(player: Player): void {
    console.log("Resuming");
    player.state = new Playing();
  }
  pause(player: Player): void {
    console.log("Already paused");
  }
}

// Context: delegates to its current state, which decides the next state
class Player {
  state: PlayerState = new Stopped();
  play(): void {
    this.state.play(this);
  }
  pause(): void {
    this.state.pause(this);
  }
}

const player = new Player();
player.play(); // Starting playback  (Stopped -> Playing)
player.pause(); // Pausing            (Playing -> Paused)
player.play(); // Resuming           (Paused -> Playing)
// State: the interface common to all states
public interface IPlayerState
{
    void Play(Player player);
    void Pause(Player player);
}

// Concrete states: each handles actions and triggers transitions
public class Stopped : IPlayerState
{
    public void Play(Player player)
    {
        Console.WriteLine("Starting playback");
        player.State = new Playing();
    }
    public void Pause(Player player) => Console.WriteLine("Nothing to pause");
}

public class Playing : IPlayerState
{
    public void Play(Player player) => Console.WriteLine("Already playing");
    public void Pause(Player player)
    {
        Console.WriteLine("Pausing");
        player.State = new Paused();
    }
}

public class Paused : IPlayerState
{
    public void Play(Player player)
    {
        Console.WriteLine("Resuming");
        player.State = new Playing();
    }
    public void Pause(Player player) => Console.WriteLine("Already paused");
}

// Context: delegates to its current state, which decides the next state
public class Player
{
    public IPlayerState State { get; set; } = new Stopped();
    public void Play() => State.Play(this);
    public void Pause() => State.Pause(this);
}

var player = new Player();
player.Play();  // Starting playback  (Stopped -> Playing)
player.Pause(); // Pausing            (Playing -> Paused)
player.Play();  // Resuming           (Paused -> Playing)
from __future__ import annotations
from typing import Protocol

# State: the interface common to all states
class PlayerState(Protocol):
    def play(self, player: "Player") -> None: ...
    def pause(self, player: "Player") -> None: ...

# Concrete states: each handles actions and triggers transitions
class Stopped:
    def play(self, player: "Player") -> None:
        print("Starting playback")
        player.state = Playing()
    def pause(self, player: "Player") -> None:
        print("Nothing to pause")

class Playing:
    def play(self, player: "Player") -> None:
        print("Already playing")
    def pause(self, player: "Player") -> None:
        print("Pausing")
        player.state = Paused()

class Paused:
    def play(self, player: "Player") -> None:
        print("Resuming")
        player.state = Playing()
    def pause(self, player: "Player") -> None:
        print("Already paused")

# Context: delegates to its current state, which decides the next state
class Player:
    def __init__(self) -> None:
        self.state: PlayerState = Stopped()
    def play(self) -> None:
        self.state.play(self)
    def pause(self) -> None:
        self.state.pause(self)

player = Player()
player.play()   # Starting playback  (Stopped -> Playing)
player.pause()  # Pausing            (Playing -> Paused)
player.play()   # Resuming           (Paused -> Playing)

Relationship to SOLID

The state pattern applies the SOLID principles in the same way the strategy pattern does. It honors the open/closed principle because a new state is a new class, so the context stays closed for modification while the set of behaviors is open for extension. It also follows the dependency inversion principle, since the context depends on the state abstraction rather than on any concrete state. And because each state class handles one part of the lifecycle, it supports the single-responsibility principle by keeping unrelated behavior apart.

State vs alternatives

  • Strategy pattern, strategy and state share the same structure: a context delegating to an interchangeable object behind a common interface. The intent differs. A strategy is picked once by the client and rarely swaps itself, while states are aware of one another and drive the transitions between them as the context moves through its lifecycle.
  • Finite-state machine tables, for large or data-driven transition graphs, a lookup table or state-machine library can be clearer than one class per state. The state pattern shines when each state carries real behavior, not just a transition entry.
  • Plain conditionals, with two or three states and simple rules, a switch on a status field is perfectly fine. Reach for the pattern once the branching is duplicated across methods or the transitions become hard to follow.

Trade-offs

  • Adds a class per state and spreads the transition logic across them, which can make the overall state graph harder to see at a glance.
  • States often need access to the context (and sometimes to each other) to trigger transitions, creating coupling that must be managed deliberately.
  • Best when an object has a well-defined lifecycle, behavior varies by state, and those variations would otherwise sprawl into repeated conditionals.

See also

  • Strategy Pattern, A behavioral design pattern that encapsulates interchangeable algorithms behind a common interface, selected at runtime.
  • Observer Pattern, A behavioral design pattern that lets dependents subscribe to a subject and receive automatic updates when its state changes, without tight coupling between them.
  • SOLID Principles, Five object-oriented design principles for maintainable, change-tolerant code.

On Wikipedia