Skip to main content

State Pattern

Definition​

The State allows an object to alter its behavior when its internal state changes. The object will appear to change its class. Each state is encapsulated in its own class, and the context delegates to the current state object.

Coffee Shop Example​

A coffee order has a lifecycle: Created β†’ Paid β†’ Brewing β†’ Ready β†’ Collected. In each state, the order responds differently to the same actions β€” you can't start brewing an unpaid order, and you can't collect an order that isn't ready.

Structure​

State Interface​

public interface IOrderState
{
void Pay(OrderContext order);
void StartBrewing(OrderContext order);
void FinishBrewing(OrderContext order);
void Collect(OrderContext order);
string GetName();
}

Context β€” The Order​

public class OrderContext
{
public string Customer { get; }
public string Coffee { get; }
public CupSize Size { get; }
private IOrderState _state;

public OrderContext(string customer, string coffee, CupSize size)
{
Customer = customer;
Coffee = coffee;
Size = size;
_state = new CreatedState();
Console.WriteLine($" [{_state.GetName()}] Order created: {size} {coffee} for {customer}");
}

public void TransitionTo(IOrderState state)
{
_state = state;
Console.WriteLine($" β†’ State: {_state.GetName()}");
}

public void Pay() => _state.Pay(this);
public void StartBrewing() => _state.StartBrewing(this);
public void FinishBrewing() => _state.FinishBrewing(this);
public void Collect() => _state.Collect(this);
public string GetState() => _state.GetName();
}

Concrete States​

public class CreatedState : IOrderState
{
public void Pay(OrderContext order)
{
Console.WriteLine($" [Payment] Charging for {order.Coffee}...");
order.TransitionTo(new PaidState());
}

public void StartBrewing(OrderContext order) =>
Console.WriteLine(" ⚠️ Cannot brew β€” order not paid yet!");

public void FinishBrewing(OrderContext order) =>
Console.WriteLine(" ⚠️ Cannot finish β€” brewing hasn't started!");

public void Collect(OrderContext order) =>
Console.WriteLine(" ⚠️ Cannot collect β€” order not ready!");

public string GetName() => "Created";
}

public class PaidState : IOrderState
{
public void Pay(OrderContext order) =>
Console.WriteLine(" ⚠️ Already paid!");

public void StartBrewing(OrderContext order)
{
Console.WriteLine($" [Barista] Starting to brew {order.Coffee}...");
order.TransitionTo(new BrewingState());
}

public void FinishBrewing(OrderContext order) =>
Console.WriteLine(" ⚠️ Cannot finish β€” brewing hasn't started!");

public void Collect(OrderContext order) =>
Console.WriteLine(" ⚠️ Cannot collect β€” order not ready!");

public string GetName() => "Paid";
}

public class BrewingState : IOrderState
{
public void Pay(OrderContext order) =>
Console.WriteLine(" ⚠️ Already paid!");

public void StartBrewing(OrderContext order) =>
Console.WriteLine(" ⚠️ Already brewing!");

public void FinishBrewing(OrderContext order)
{
Console.WriteLine($" [Barista] {order.Coffee} is done!");
order.TransitionTo(new ReadyState());
}

public void Collect(OrderContext order) =>
Console.WriteLine(" ⚠️ Cannot collect β€” still brewing!");

public string GetName() => "Brewing";
}

public class ReadyState : IOrderState
{
public void Pay(OrderContext order) =>
Console.WriteLine(" ⚠️ Already paid!");

public void StartBrewing(OrderContext order) =>
Console.WriteLine(" ⚠️ Already brewed!");

public void FinishBrewing(OrderContext order) =>
Console.WriteLine(" ⚠️ Already finished!");

public void Collect(OrderContext order)
{
Console.WriteLine($" [Pickup] {order.Customer} collected their {order.Coffee}! πŸŽ‰");
order.TransitionTo(new CollectedState());
}

public string GetName() => "Ready";
}

public class CollectedState : IOrderState
{
public void Pay(OrderContext order) =>
Console.WriteLine(" ⚠️ Order already completed.");

public void StartBrewing(OrderContext order) =>
Console.WriteLine(" ⚠️ Order already completed.");

public void FinishBrewing(OrderContext order) =>
Console.WriteLine(" ⚠️ Order already completed.");

public void Collect(OrderContext order) =>
Console.WriteLine(" ⚠️ Order already collected.");

public string GetName() => "Collected";
}

Client​

var order = new OrderContext("Alice", "Latte", CupSize.Large);

// Try to brew before paying β€” blocked!
order.StartBrewing();
// ⚠️ Cannot brew β€” order not paid yet!

// Normal flow
order.Pay();
order.StartBrewing();
order.FinishBrewing();
order.Collect();

// Try to pay again β€” blocked!
order.Pay();
// ⚠️ Order already completed.

// Output:
// [Created] Order created: Large Latte for Alice
// ⚠️ Cannot brew β€” order not paid yet!
// [Payment] Charging for Latte...
// β†’ State: Paid
// [Barista] Starting to brew Latte...
// β†’ State: Brewing
// [Barista] Latte is done!
// β†’ State: Ready
// [Pickup] Alice collected their Latte! πŸŽ‰
// β†’ State: Collected
// ⚠️ Order already completed.
tip

Each state class handles the same methods differently. The OrderContext delegates to the current state. Invalid transitions are handled gracefully β€” no if (state == "Brewing") checks needed.

State vs Strategy​

StateStrategy
Who changes itThe object itself (internal transitions)Client (external injection)
KnowledgeClient doesn't know about statesClient chooses the strategy
TransitionsStates know about other statesStrategies don't know about each other
IntentBehavior changes with stateAlgorithm changes by choice

.NET Real-World Usage​

  • ASP.NET Core IAuthenticationHandler β€” different behavior per auth state
  • Task<T> lifecycle β€” Created β†’ WaitingForActivation β†’ Running β†’ Completed/Faulted
  • Workflow engines (Elsa, Orleans grains) β€” state-driven processing
  • Game development β€” character states (idle, running, jumping, attacking)

When to Use​

  • An object's behavior depends on its state, and it must change at runtime
  • You have large if/switch blocks that check state in many methods
  • State transitions are complex with many rules

When NOT to Use​

  • There are only 2 states β€” a simple boolean flag suffices
  • State logic is trivial β€” enum + switch is simpler
  • States don't have complex behavior differences

Key Takeaways​

  • State encapsulates each state's behavior in its own class
  • The context delegates to the current state object
  • State transitions happen internally β€” the context calls TransitionTo()
  • Eliminates large conditional blocks β€” each state class handles its own logic

Interview Questions​

Q: How is State different from a finite state machine (FSM)? State pattern is an implementation of FSM in OOP. Each state class represents a state, and transitions are method calls. A traditional FSM uses a state table or switch β€” State pattern uses polymorphism.

Q: Should states be singletons or new instances? If states are stateless (no instance fields), they can be singletons β€” shared across all contexts. If states hold data (like a timer), create new instances per context.

Q: What's the State pattern's relationship to the Open/Closed Principle? Adding a new state (e.g., CancelledState) adds a new class without modifying existing states or the context. Each state is closed for modification, open for extension.