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.
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β
| State | Strategy | |
|---|---|---|
| Who changes it | The object itself (internal transitions) | Client (external injection) |
| Knowledge | Client doesn't know about states | Client chooses the strategy |
| Transitions | States know about other states | Strategies don't know about each other |
| Intent | Behavior changes with state | Algorithm 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/switchblocks 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+switchis 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.
Related Topicsβ
- Strategy β similar structure, client-driven (not state-driven)
- Template Method β fixed algorithm with variable steps
- Command β can trigger state transitions
- SOLID β OCP & SRP β each state class has one responsibility