Behavioral Pattern Pattern 20 of 23

State

An order that's already Delivered shouldn't be cancellable the same way a Pending order is — the same method call, cancel(), needs to mean something completely different depending on what state the order is currently in. The State pattern handles this by giving each state its own class that implements the shared behavior its own way, and letting the object simply swap which state object it's currently delegating to.

⚠️ The Problem

Picture an Order class with a status field that can be Pending, Paid, Shipped, Delivered, or Cancelled. Every method — pay(), ship(), cancel(), deliver() — needs a big switch or if/else chain on status to decide what's even allowed: you can't ship a Pending order, you can't cancel a Delivered one, and paying twice should probably be an error. As more statuses and rules get added, every one of those methods grows another branch, the branches have to stay in sync with each other across the whole class, and it becomes very easy to forget an edge case buried in the fifth else if down.

✅ The Solution

Give each status its own class — PendingState, PaidState, ShippedState, and so on — all implementing a common OrderState interface with methods like pay(), ship(), and cancel(). The Order object holds a reference to its current state object and simply forwards every call to it: order.cancel() becomes this.state.cancel(this). Each state class only has to know its own valid transitions — PendingState.cancel() moves the order to CancelledState, while DeliveredState.cancel() might just throw or no-op. Transitioning states is just reassigning which state object Order points to next.

Example 1 — an order status state machine

Primary Example — TypeScript
interface OrderState {
  pay(order: Order): void;
  ship(order: Order): void;
  cancel(order: Order): void;
  name: string;
}

class PendingState implements OrderState {
  name = "Pending";
  pay(order: Order) { order.setState(new PaidState()); }
  ship(order: Order) { console.log("Can't ship — payment not received yet."); }
  cancel(order: Order) { order.setState(new CancelledState()); }
}

class PaidState implements OrderState {
  name = "Paid";
  pay() { console.log("Already paid."); }
  ship(order: Order) { order.setState(new ShippedState()); }
  cancel(order: Order) { order.setState(new CancelledState()); }
}

class ShippedState implements OrderState {
  name = "Shipped";
  pay() { console.log("Already paid."); }
  ship() { console.log("Already shipped."); }
  cancel() { console.log("Too late — order has already shipped."); }
}

class CancelledState implements OrderState {
  name = "Cancelled";
  pay() { console.log("Order is cancelled."); }
  ship() { console.log("Order is cancelled."); }
  cancel() { console.log("Already cancelled."); }
}

class Order {
  private state: OrderState = new PendingState();

  setState(state: OrderState) { this.state = state; }
  pay() { this.state.pay(this); }
  ship() { this.state.ship(this); }
  cancel() { this.state.cancel(this); }
  get status() { return this.state.name; }
}

const order = new Order();
order.ship();          // "Can't ship — payment not received yet."
order.pay();
console.log(order.status); // "Paid"
order.ship();
console.log(order.status); // "Shipped"
order.cancel();         // "Too late — order has already shipped."

Notice Order itself has no if statements about status at all — it just forwards every call to whatever OrderState object it currently holds. Adding a ReturnedState later means writing one new class, not touching every method on Order.

Example 2 — a secondary context: a media player

Secondary Example — Python
class PlayerState:
    def play(self, player): raise NotImplementedError
    def pause(self, player): raise NotImplementedError
    def stop(self, player): raise NotImplementedError

class PlayingState(PlayerState):
    def play(self, player):
        print("Already playing.")
    def pause(self, player):
        print("Pausing playback.")
        player.state = PausedState()
    def stop(self, player):
        print("Stopping playback.")
        player.state = StoppedState()

class PausedState(PlayerState):
    def play(self, player):
        print("Resuming playback.")
        player.state = PlayingState()
    def pause(self, player):
        print("Already paused.")
    def stop(self, player):
        print("Stopping from pause.")
        player.state = StoppedState()

class StoppedState(PlayerState):
    def play(self, player):
        print("Starting playback from the beginning.")
        player.state = PlayingState()
    def pause(self, player):
        print("Nothing to pause — playback is stopped.")
    def stop(self, player):
        print("Already stopped.")

class MediaPlayer:
    def __init__(self):
        self.state = StoppedState()

    def play(self):  self.state.play(self)
    def pause(self): self.state.pause(self)
    def stop(self):  self.state.stop(self)

player = MediaPlayer()
player.play()   # Starting playback from the beginning.
player.pause()  # Pausing playback.
player.pause()  # Already paused.
player.play()   # Resuming playback.

Same shape as the order example: MediaPlayer never checks "what state am I in" with an if — it just calls the method on whichever PlayerState object it's currently holding, and that object decides what happens next, including what it transitions to.

👍 When To Use It

  • An object's behavior needs to change substantially based on an internal state, and that state changes at runtime through a well-defined set of transitions.
  • You keep finding the same status/mode field checked with if/else or switch across many methods of the same class.
  • New states get added over time, and you want adding one to mean "add a class," not "edit every existing method."

👎 When NOT To Use It

  • When there are only two simple states and the behavior difference is trivial — a boolean flag and a single if is easier to read than two classes and an interface.
  • When the states rarely or never change after the object is created — State earns its keep specifically because of runtime transitions between many behaviors.
  • When the "states" don't actually share a common interface of operations — if each one needs a wildly different set of methods, forcing them into one State interface can be more awkward than the switch statement it replaced.

❓ FAQ

Isn't this exactly the same as the Strategy pattern?

Structurally, yes — both give you an interface with interchangeable implementations that a context object delegates to, and the code often looks nearly identical. The difference is in intent and where the decision comes from. State is about an object's behavior changing as its own internal state evolves over time — the state objects often know about and trigger transitions to each other (like PendingState.pay() switching to PaidState), and the context rarely, if ever, gets told which state to use from outside. Strategy is about a client choosing which interchangeable algorithm to use — the strategy is typically passed in from outside by whoever constructs or configures the context, has no notion of "the next strategy," and doesn't represent a point in the object's own lifecycle. If the implementations know about and switch to each other, it's State; if the caller picks one and hands it in, it's Strategy.

Who decides which state to transition to — the Order, or the State object?

In the classic version of the pattern, the current State object itself decides and calls order.setState(...), exactly as in the example above — the context class stays completely ignorant of the transition rules. Some implementations instead centralize the transition table in the context for easier auditing of "what can go to what," trading a bit of that decentralization for a single place to read all the rules. Both are valid; the example here uses the more common, fully decentralized version.

Do I need a separate class for every single state, even trivial ones?

Not necessarily — if a handful of states share almost all their behavior, a shared base class with template-style hooks, or even flyweight-shared stateless instances (since PendingState above holds no per-order data, one shared instance could serve every Order), can cut down on allocations and boilerplate. The core requirement is just that each distinct behavior set lives in its own place, not that every state needs a bespoke from-scratch class.

🙋 There Are No Dumb Questions

Q: Doesn't creating a new state object on every transition waste memory?
A: For most applications, no — state objects are usually tiny and short-lived, and creating one per transition is negligible compared to the readability win. If profiling shows it actually matters (very high-frequency transitions, e.g. a game running at 60fps), stateless state objects can be pre-created once and shared as singletons/flyweights across every context that needs them, since they hold no per-instance data of their own.

✅ Quick Check

In the State pattern, what typically decides which state to transition to next?


Next up: what if you need several interchangeable ways to perform the same task — like different pricing or routing algorithms — without a giant if/else picking between them? That's the Strategy pattern, and it looks a lot like State on the surface but solves a different problem.

← Back to
Next →