In real applications, objects rarely have fixed behavior. Most of the time, the way they react depends on the internal state they are in. For example:
-
an order can be New, Processed, Delivered, or Cancelled
-
a document can be Draft, Published, or Archived
-
a user can be Active, Suspended, or Blocked
A naive approach would involve using many if / switch statements in the code, which quickly leads to:
-
hard-to-read code
-
violation of the Open/Closed principle
-
great difficulties when adding new states
State Pattern elegantly solves this problem.
What is State Pattern?
State Pattern allows an object to change its behavior when its internal state changes. The object will appear to have changed its class.
The main idea is:
-
each state is represented by a separate class
-
the state-specific behavior is moved into that class
-
the main object (context) delegates behavior to the current state
Problem: state-dependent behavior
Let's take a simple example: an order (Order).
Wrong approach (anti-pattern)
public enum OrderStatus
{
New,
Paid,
Shipped,
Cancelled
}
public class Order
{
public OrderStatus Status { get; set; }
public void Process()
{
if (Status == OrderStatus.New)
{
Console.WriteLine("Processing new order");
}
else if (Status == OrderStatus.Paid)
{
Console.WriteLine("Preparing shipment");
}
else if (Status == OrderStatus.Shipped)
{
Console.WriteLine("Order has already been shipped");
}
else if (Status == OrderStatus.Cancelled)
{
throw new InvalidOperationException("Order is cancelled");
}
}
}
❌ Problems:
-
scattered logic
-
methods that grow uncontrollably
-
frequent changes to the
Orderclass
Solution: State Pattern
Pattern structure
-
State – common interface for all states
-
ConcreteState – concrete implementations for each state
-
Context – the object that changes its behavior
Implementation in C#
1. The IOrderState interface
public interface IOrderState
{
void Process(OrderContext context);
}
2. Concrete states
The NewOrderState state
public class NewOrderState : IOrderState
{
public void Process(OrderContext context)
{
Console.WriteLine("Processing new order");
context.SetState(new PaidOrderState());
}
}
The PaidOrderState state
public class PaidOrderState : IOrderState
{
public void Process(OrderContext context)
{
Console.WriteLine("Preparing shipment");
context.SetState(new ShippedOrderState());
}
}
The ShippedOrderState state
public class ShippedOrderState : IOrderState
{
public void Process(OrderContext context)
{
Console.WriteLine("Order has already been shipped");
}
}
The CancelledOrderState state
public class CancelledOrderState : IOrderState
{
public void Process(OrderContext context)
{
throw new InvalidOperationException("Order is cancelled");
}
}
3. The OrderContext context
public class OrderContext
{
private IOrderState _state;
public OrderContext(IOrderState initialState)
{
_state = initialState;
}
public void SetState(IOrderState state)
{
_state = state;
}
public void Process()
{
_state.Process(this);
}
}
4. Usage
var order = new OrderContext(new NewOrderState());
order.Process(); // New -> Paid
order.Process(); // Paid -> Shipped
order.Process(); // Shipped
Advantages
✅ eliminates if / switch
✅ respects the Open/Closed Principle
✅ each state is isolated and easy to test
✅ cleaner and extensible code
Disadvantages
⚠️ larger number of classes
⚠️ can be overkill for simple scenarios
When to use State Pattern?
✔️ behavior changes significantly depending on the state
✔️ you have many transitions between states
✔️ you want to avoid complex conditional logic
❌ NOT recommended when you have only 1–2 simple states
Difference from Strategy Pattern
| State | Strategy |
|---|---|
| The state changes from inside | The strategy changes from outside |
| State-dependent flow | Interchangeable algorithms |
Conclusion
State Pattern is ideal for modeling real processes (workflows, orders, documents, payments), where behavior evolves over time.
When applied correctly, it leads to:
-
more readable code
-
fewer bugs
-
true extensibility