A traffic light is always in exactly one state, and its behavior depends entirely on which one:
- Red — cars stop. After a timer, it becomes Green.
- Green — cars go. After a timer, it becomes Yellow.
- Yellow — cars slow. After a timer, it becomes Red.
Same object, but what it does — and what it becomes next — changes completely based on its current state. And crucially, the light switches itself. Red decides when to become Green. Nobody from outside picks the next state.
That's the State pattern: an object changes its own behavior when its internal state changes — as if it became a different class.
The problem it solves
Without it, you get a mess of flags and ifs scattered across every method:
public void Play()
{
if (_state == "playing") Console.WriteLine("Already playing");
else if (_state == "paused") { _state = "playing"; Console.WriteLine("Resumed"); }
else if (_state == "stopped") { _state = "playing"; Console.WriteLine("Starting"); }
}
public void Pause()
{
if (_state == "playing") { ... }
else if (_state == "paused") { ... }
// the same tangle repeated in every method
}
Every method re-checks the state. Add a new state and you edit every method. This is the classic "state machine as spaghetti."
The State way
Each state becomes its own class that knows two things: how to behave, and which state to move to next.
The state interface:
public interface IPlayerState
{
void Play(MediaPlayer player);
void Pause(MediaPlayer player);
}
The context holds the current state and delegates to it:
public class MediaPlayer
{
public IPlayerState State { get; set; }
public MediaPlayer() => State = new StoppedState(); // start here
public void Play() => State.Play(this); // delegate to current state
public void Pause() => State.Pause(this);
}
Each state is a class — and it decides the next state:
public class StoppedState : IPlayerState
{
public void Play(MediaPlayer player)
{
Console.WriteLine("Starting playback");
player.State = new PlayingState(); // the state switches itself
}
public void Pause(MediaPlayer player) => Console.WriteLine("Can't pause — already stopped");
}
public class PlayingState : IPlayerState
{
public void Play(MediaPlayer player) => Console.WriteLine("Already playing");
public void Pause(MediaPlayer player)
{
Console.WriteLine("Pausing");
player.State = new PausedState();
}
}
public class PausedState : IPlayerState
{
public void Play(MediaPlayer player)
{
Console.WriteLine("Resuming");
player.State = new PlayingState();
}
public void Pause(MediaPlayer player) => Console.WriteLine("Already paused");
}
Watch it flow:
var player = new MediaPlayer(); // Stopped
player.Play(); // "Starting playback" → Playing
player.Pause(); // "Pausing" → Paused
player.Play(); // "Resuming" → Playing
player.Play(); // "Already playing"
No if (_state == ...) anywhere. Each state class handles its own behavior and points to the next. Adding a new state — say a buffering state — means writing one new class; no existing method gets touched.
Strategy vs State — the confusion, settled
The code looks almost identical. Both plug in an object and delegate to it. The difference is who decides, and whether it changes itself:
- Who picks the plugged-in object? Strategy: you, from outside. State: the object switches itself.
- Does it change during use? Strategy: no — pick once, it runs. State: yes — it flips as things happen.
- Do the objects know each other? Strategy: no, they're independent algorithms. State: yes, each state knows the next.
- Analogy. Strategy is Google Maps — you pick car vs walking. State is a traffic light — red becomes green on its own.
The tell: in Strategy, the context never reassigns its own strategy — you do, from outside. In State, PlayingState sets player.State = new PausedState() — the states rewrite the context from inside. Self-transitioning means State. Externally-chosen means Strategy.
Where you've seen it
- Order lifecycle — Pending, Paid, Shipped, Delivered, each stage allowing different actions.
- Workflow and approval engines — Draft, Submitted, Approved, Published.
- Game character states — Idle, Running, Jumping, Attacking.
- Connection states — Disconnected, Connecting, Connected, Reconnecting.
- UI wizards — each step enables or disables different buttons.
One line to remember
State = traffic light. The object holds one state at a time, behaves according to it, and switches itself to the next — no giant if-else tangle.
Part 13 of a series on must-know design patterns in C#, explained the simple way. Earlier parts covered Singleton through Iterator. Next up: Template Method — defining a skeleton and letting subclasses fill in the steps.
Top comments (0)