State Design Pattern in .NET Core API
A practical State pattern guide in C# with classic state objects and enum-driven state machine styles.

Intro
Objects often behave differently depending on what phase they are in. The State pattern keeps those phase changes explicit by moving behavior into state objects instead of piling conditionals into one class.
What Is the State Pattern?
State is a behavioral design pattern that lets an object alter its behavior when its internal state changes.
In .NET systems, it is useful for workflow engines, order lifecycles, approval states, and request processing states.
State Class Example
The example below models an order that changes behavior as it moves through states.
public interface IOrderState{ void Pay(OrderContext context); void Ship(OrderContext context);}
public sealed class OrderContext{ public IOrderState State { get; set; }
public OrderContext(IOrderState state) => State = state;
public void Pay() => State.Pay(this); public void Ship() => State.Ship(this);}
public sealed class CreatedState : IOrderState{ public void Pay(OrderContext context) => context.State = new PaidState(); public void Ship(OrderContext context) { }}
public sealed class PaidState : IOrderState{ public void Pay(OrderContext context) { } public void Ship(OrderContext context) => context.State = new ShippedState();}
public sealed class ShippedState : IOrderState{ public void Pay(OrderContext context) { } public void Ship(OrderContext context) { }}Why This Works
The context delegates behavior to the current state object. When the state changes, the context’s behavior changes with it, which removes long conditional chains and makes transitions explicit.
When to Use It
State is a good fit when an object has clear lifecycle phases.
- order or ticket lifecycles
- approval and workflow engines
- communication protocol states
- UI mode changes
- request processing phases
If the behavior differences are small and stable, a simple switch may be enough.
Important Note for ASP.NET Core
In ASP.NET Core, state is often modeled inside workflow services or domain models, not controllers. Keep transitions in the application layer so endpoint code stays thin and predictable.
builder.Services.AddScoped<OrderContext>();That keeps the state machine testable without coupling it to HTTP concerns.
Core Components of the Pattern
| Part | Purpose | Example in this article |
|---|---|---|
| Context | Holds current state | OrderContext |
| State interface | Defines behavior contract | IOrderState |
| Concrete states | Implement state-specific behavior | CreatedState, PaidState, ShippedState |
| Client | Triggers transitions | API layer or service layer |
Common State Variations
You will commonly apply the State pattern in these two forms:
Classic State Objects
Each state is a separate class that owns its behavior and transition rules.
Reference implementation: Classic State Objects
public sealed class PaidState : IOrderState { }State Machine
An enum plus switch statement centralizes transitions in one place.
Reference implementation: State Machine Enum + Switch
public enum OrderState{ Created, Paid, Shipped}How the Variations Differ
| Variation | Structure | Extensibility | Best use case |
|---|---|---|---|
| Classic State Objects | One class per state | High | Complex lifecycles |
| State Machine | Enum + switch | Lower | Small, static workflows |
SOLID Principles Behind It
SRP - Single Responsibility Principle
Each state class handles one phase of behavior.
OCP - Open/Closed Principle
You can add new states without rewriting every transition path.
LSP - Liskov Substitution Principle
Any concrete state can stand in for the state interface.
Advantages and Disadvantages
Advantages
- removes conditional complexity
- makes transitions explicit and testable
- keeps lifecycle-specific behavior isolated
- makes adding phases easier
Disadvantages (and Optional Alternatives)
- many states can create more classes Optional pattern: Template Method (when the sequence is fixed and only steps vary).
- simple workflows may not need the abstraction Optional pattern: Enum + switch (when transitions are trivial).
- state transition bugs can be subtle if not well tested Optional pattern: Memento (when restoring exact prior state is safer than modeling transitions).
UML Diagram
classDiagram class OrderContext class IOrderState { <<interface>> +Pay(context) +Ship(context) } class CreatedState class PaidState class ShippedState
OrderContext --> IOrderState CreatedState ..|> IOrderState PaidState ..|> IOrderState ShippedState ..|> IOrderStateA Quick Usage Example
var order = new OrderContext(new CreatedState());order.Pay();order.Ship();The context changes behavior as it moves through states, without exposing transition logic to callers.
How to Validate the Pattern
Use these checks to confirm your State implementation is healthy:
- each state should own only state-specific behavior
- transitions should be explicit and covered by tests
- the context should not contain large conditional branches
- invalid transitions should be prevented or handled clearly
- state classes should be swappable without changing callers
Is there any other way to check our implementations
- yes, add unit tests for every legal and illegal transition
- add workflow tests around the full lifecycle in your service layer
Summary
The State pattern makes lifecycle behavior easier to understand and extend by moving phase-specific logic into dedicated state objects. In .NET APIs, it is a strong choice when workflows have clear transitions and behavior changes with those transitions.




