Learnixo
Back to blog
Backend Systemsintermediate

State — Behaviour Changes With Internal State

The State pattern in C#: eliminate large switch statements by encoding state as classes. Build an order state machine, vending machine, and workflow engine with clean transitions.

Asma Hafeez KhanMay 24, 20264 min read
csharpdesign-patternsstatebehavioraldotnetstate-machine
Share:𝕏

State — Behaviour Changes With Internal State

The State pattern allows an object to change its behaviour when its internal state changes. Instead of a giant switch/if-else on state, each state is a class with its own behaviour. The object delegates to its current state object.


The Problem

C#
// Without State: complex switch/if — hard to add new states
public class Order
{
    public string Status { get; set; } = "Draft";

    public void Submit()
    {
        switch (Status)
        {
            case "Draft":    Status = "Submitted"; break;
            case "Submitted": throw new InvalidOperationException("Already submitted");
            case "Shipped":  throw new InvalidOperationException("Cannot resubmit shipped order");
            // ... growing every time we add a state
        }
    }
}

State Pattern Implementation

C#
// State interface
public interface IOrderState
{
    void Submit(Order order);
    void Pay(Order order);
    void Ship(Order order);
    void Cancel(Order order);
    string StatusName { get; }
}

// Context — delegates all behaviour to current state
public class Order
{
    private IOrderState _state = new DraftState();

    public int     Id       { get; init; }
    public decimal Total    { get; init; }
    public string  Status   => _state.StatusName;

    internal void TransitionTo(IOrderState state)
    {
        Console.WriteLine($"Order {Id}: {_state.StatusName} → {state.StatusName}");
        _state = state;
    }

    public void Submit()  => _state.Submit(this);
    public void Pay()     => _state.Pay(this);
    public void Ship()    => _state.Ship(this);
    public void Cancel()  => _state.Cancel(this);
}

// Concrete States
public class DraftState : IOrderState
{
    public string StatusName => "Draft";

    public void Submit(Order order) => order.TransitionTo(new SubmittedState());
    public void Pay(Order order)    => throw new InvalidOperationException("Submit before paying");
    public void Ship(Order order)   => throw new InvalidOperationException("Not yet paid");
    public void Cancel(Order order) => order.TransitionTo(new CancelledState());
}

public class SubmittedState : IOrderState
{
    public string StatusName => "Submitted";

    public void Submit(Order order) => throw new InvalidOperationException("Already submitted");
    public void Pay(Order order)    => order.TransitionTo(new PaidState());
    public void Ship(Order order)   => throw new InvalidOperationException("Payment required first");
    public void Cancel(Order order) => order.TransitionTo(new CancelledState());
}

public class PaidState : IOrderState
{
    public string StatusName => "Paid";

    public void Submit(Order order) => throw new InvalidOperationException("Already submitted");
    public void Pay(Order order)    => throw new InvalidOperationException("Already paid");
    public void Ship(Order order)   => order.TransitionTo(new ShippedState());
    public void Cancel(Order order) => order.TransitionTo(new RefundingState());
}

public class ShippedState : IOrderState
{
    public string StatusName => "Shipped";

    public void Submit(Order order) => throw new InvalidOperationException("Order shipped");
    public void Pay(Order order)    => throw new InvalidOperationException("Order shipped");
    public void Ship(Order order)   => throw new InvalidOperationException("Already shipped");
    public void Cancel(Order order) => throw new InvalidOperationException("Cannot cancel shipped order");
}

public class CancelledState : IOrderState
{
    public string StatusName => "Cancelled";
    public void Submit(Order order) => throw new InvalidOperationException("Order cancelled");
    public void Pay(Order order)    => throw new InvalidOperationException("Order cancelled");
    public void Ship(Order order)   => throw new InvalidOperationException("Order cancelled");
    public void Cancel(Order order) => throw new InvalidOperationException("Already cancelled");
}

public class RefundingState : IOrderState
{
    public string StatusName => "Refunding";
    public void Submit(Order order) => throw new InvalidOperationException("Order being refunded");
    public void Pay(Order order)    => throw new InvalidOperationException("Order being refunded");
    public void Ship(Order order)   => throw new InvalidOperationException("Order being refunded");
    public void Cancel(Order order) => throw new InvalidOperationException("Already cancelled");
}

// Usage
var order = new Order { Id = 1, Total = 99.99m };
order.Submit();   // Draft → Submitted
order.Pay();      // Submitted → Paid
order.Ship();     // Paid → Shipped

Lightweight State with Enum + Record

C#
// For simple state machines without complex per-state behaviour
public enum TrafficLight { Red, Yellow, Green }

public class TrafficLightController
{
    private TrafficLight _current = TrafficLight.Red;

    public void Advance()
    {
        _current = _current switch
        {
            TrafficLight.Red    => TrafficLight.Green,
            TrafficLight.Green  => TrafficLight.Yellow,
            TrafficLight.Yellow => TrafficLight.Red,
            _ => throw new InvalidOperationException("Unknown state"),
        };
    }

    public string Current => _current.ToString();
}

Interview Answer

"The State pattern encodes each state as a class that implements a common interface. The context object delegates all state-dependent behaviour to its current state object — no switch statement needed. Adding a new state means adding a new class; existing states don't change (open/closed principle). The pattern shines for domain objects with complex lifecycles: orders (Draft → Submitted → Paid → Shipped), workflows (Pending → InReview → Approved → Published), and connection objects (Disconnected → Connecting → Connected → Failed). The trade-off: simple state machines with 2–3 states and limited transitions are cleaner with an enum + switch expression — the full State pattern is justified when behaviour per state is complex or states need to grow independently."

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.