BrandGhost
State Design Pattern in C#: Complete Guide with Examples

State Design Pattern in C#: Complete Guide with Examples

State Design Pattern in C#: Complete Guide with Examples

When an object needs to change its behavior based on its internal condition, the state design pattern in C# is the behavioral pattern designed to handle that cleanly. Instead of scattering conditional logic across your codebase, you encapsulate each state as its own class and let the object delegate behavior to whichever state is active. The result is code that's easier to read, extend, and maintain -- especially when the number of states and transitions grows over time.

In this complete guide, we'll walk through everything you need to know about the state design pattern -- from the core components and a hands-on C# implementation to finite state machines, state transitions, the relationship to the strategy pattern, and practical considerations for production use. By the end, you'll have working code examples and a clear understanding of when this pattern is the right fit for your application.

What Is the State Design Pattern?

The state design pattern is a behavioral pattern from the Gang of Four (GoF) catalog that allows an object to alter its behavior when its internal state changes. From the outside, the object appears to change its class. The pattern eliminates large conditional blocks -- like sprawling switch statements or deeply nested if-else chains -- by replacing them with polymorphism.

Think of a vending machine. When it's idle, inserting a coin moves it to a "has money" state. When it has money, selecting a product dispenses it and returns to idle. Trying to dispense without paying does nothing. Each state defines its own rules for how the machine responds to user actions, and the machine delegates to the current state object. You don't need a massive switch block that checks the current mode -- each state handles its own logic.

The pattern involves three key participants: the State interface (or abstract class), Concrete States, and the Context. The state interface declares the methods that all concrete states must implement. Each concrete state encapsulates the behavior associated with a particular condition. The context is the object whose behavior changes -- it holds a reference to the current state and delegates requests to it.

The key insight is that state-specific behavior lives in state objects, not in the context. The context doesn't need to know the details of any individual state. It only knows the state interface. This separation makes adding new states straightforward: you create a new class, implement the interface, and wire it into the transition logic. Existing states and the context don't change.

Core Components of the State Design Pattern

Each participant in the pattern has a well-defined responsibility. Getting these boundaries right is what keeps the pattern manageable as your system grows.

The State Interface declares the methods that represent the context's state-dependent behavior. Every concrete state implements this interface, providing its own version of each method. In C#, this is typically an interface or an abstract class. If you have shared behavior across some states, an abstract class with virtual methods can reduce duplication.

Concrete State classes implement the state interface and define the behavior for a specific condition. Each concrete state encapsulates exactly one state's worth of logic. When the context delegates a method call to its current state, the concrete state handles it according to its own rules -- and it may trigger a transition to a different state.

The Context is the object that clients interact with. It maintains a reference to the current state object and exposes methods that delegate to that state. The context provides a way for state objects to trigger transitions, typically through a method like SetState or by exposing a property. Critically, the context doesn't contain state-specific logic itself -- it's a thin wrapper that routes calls to the active state. This is where inversion of control shows up: the context hands off behavior decisions to the state objects rather than making those decisions internally.

Implementing the State Pattern in C#

Let's build a practical example: an order processing system where an order moves through several states -- Pending, Confirmed, Shipped, and Delivered. Each state defines what actions are allowed and what happens when those actions are triggered. This maps directly to how real-world order workflows operate.

Defining the State Interface

The state interface declares methods for every action that the context supports. Each state will provide its own implementation:

public interface IOrderState
{
    void Confirm(OrderContext order);
    void Ship(OrderContext order);
    void Deliver(OrderContext order);
    void Cancel(OrderContext order);
    string GetStatus();
}

Five methods. Each one receives the OrderContext so that the state can trigger a transition by calling a method on the context. GetStatus returns a human-readable label for the current state. Every concrete state implements this interface, and the context delegates to it without knowing which state is active.

Creating Concrete State Classes

Each concrete state class implements IOrderState and defines the behavior for its specific condition. Here's the pending state -- the starting point for every new order:

using System;

public sealed class PendingState : IOrderState
{
    public void Confirm(OrderContext order)
    {
        Console.WriteLine("Order confirmed.");
        order.SetState(new ConfirmedState());
    }

    public void Ship(OrderContext order)
    {
        Console.WriteLine(
            "Cannot ship. Order has not been confirmed.");
    }

    public void Deliver(OrderContext order)
    {
        Console.WriteLine(
            "Cannot deliver. Order has not been shipped.");
    }

    public void Cancel(OrderContext order)
    {
        Console.WriteLine("Order cancelled.");
        order.SetState(new CancelledState());
    }

    public string GetStatus() => "Pending";
}

The pending state only allows two actions: confirming the order and cancelling it. Shipping and delivering are invalid in this state, so those methods print a message and do nothing else. When Confirm is called, the state transitions the context to ConfirmedState. When Cancel is called, it transitions to CancelledState.

Here's the confirmed state:

using System;

public sealed class ConfirmedState : IOrderState
{
    public void Confirm(OrderContext order)
    {
        Console.WriteLine("Order is already confirmed.");
    }

    public void Ship(OrderContext order)
    {
        Console.WriteLine("Order shipped.");
        order.SetState(new ShippedState());
    }

    public void Deliver(OrderContext order)
    {
        Console.WriteLine(
            "Cannot deliver. Order has not been shipped.");
    }

    public void Cancel(OrderContext order)
    {
        Console.WriteLine("Confirmed order cancelled.");
        order.SetState(new CancelledState());
    }

    public string GetStatus() => "Confirmed";
}

Once confirmed, the order can be shipped or cancelled, but it can't be confirmed again or delivered directly. Here are the shipped, delivered, and cancelled states:

using System;

public sealed class ShippedState : IOrderState
{
    public void Confirm(OrderContext order)
    {
        Console.WriteLine(
            "Cannot confirm. Order is already shipped.");
    }

    public void Ship(OrderContext order)
    {
        Console.WriteLine("Order is already shipped.");
    }

    public void Deliver(OrderContext order)
    {
        Console.WriteLine("Order delivered.");
        order.SetState(new DeliveredState());
    }

    public void Cancel(OrderContext order)
    {
        Console.WriteLine(
            "Cannot cancel. Order is already shipped.");
    }

    public string GetStatus() => "Shipped";
}

public sealed class DeliveredState : IOrderState
{
    public void Confirm(OrderContext order)
    {
        Console.WriteLine(
            "Cannot confirm. Order is already delivered.");
    }

    public void Ship(OrderContext order)
    {
        Console.WriteLine(
            "Cannot ship. Order is already delivered.");
    }

    public void Deliver(OrderContext order)
    {
        Console.WriteLine("Order is already delivered.");
    }

    public void Cancel(OrderContext order)
    {
        Console.WriteLine(
            "Cannot cancel. Order is already delivered.");
    }

    public string GetStatus() => "Delivered";
}

public sealed class CancelledState : IOrderState
{
    public void Confirm(OrderContext order)
    {
        Console.WriteLine(
            "Cannot confirm. Order is cancelled.");
    }

    public void Ship(OrderContext order)
    {
        Console.WriteLine(
            "Cannot ship. Order is cancelled.");
    }

    public void Deliver(OrderContext order)
    {
        Console.WriteLine(
            "Cannot deliver. Order is cancelled.");
    }

    public void Cancel(OrderContext order)
    {
        Console.WriteLine("Order is already cancelled.");
    }

    public string GetStatus() => "Cancelled";
}

DeliveredState and CancelledState are terminal states -- no valid transitions exist from either. Every method in these classes rejects the action with a descriptive message. This is a common pattern in state machine design: terminal states guard against invalid transitions by ignoring or rejecting all actions.

Building the Context Class

The context holds the current state and delegates all actions to it. It also exposes a SetState method that concrete states call to trigger transitions:

using System;

public sealed class OrderContext
{
    private IOrderState _currentState;

    public OrderContext()
    {
        _currentState = new PendingState();
    }

    public void SetState(IOrderState state)
    {
        _currentState = state;
        Console.WriteLine(
            $"State changed to: {_currentState.GetStatus()}");
    }

    public void Confirm() => _currentState.Confirm(this);

    public void Ship() => _currentState.Ship(this);

    public void Deliver() => _currentState.Deliver(this);

    public void Cancel() => _currentState.Cancel(this);

    public string Status => _currentState.GetStatus();
}

The context is clean and minimal. It doesn't contain any state-specific logic. Every method call is routed to _currentState, and the state objects handle the rest. The constructor initializes the order in PendingState, which is the natural starting point.

Putting It All Together

Here's how a client uses the order system:

using System;

var order = new OrderContext();
Console.WriteLine($"Current status: {order.Status}");
// Output: Current status: Pending

order.Ship();
// Output: Cannot ship. Order has not been confirmed.

order.Confirm();
// Output: Order confirmed.
// Output: State changed to: Confirmed

order.Ship();
// Output: Order shipped.
// Output: State changed to: Shipped

order.Cancel();
// Output: Cannot cancel. Order is already shipped.

order.Deliver();
// Output: Order delivered.
// Output: State changed to: Delivered

Console.WriteLine($"Final status: {order.Status}");
// Output: Final status: Delivered

The client doesn't need to know anything about the internal state classes. It calls methods on the context, and the context delegates to whichever state is active. Invalid actions are handled gracefully by the current state, and valid actions trigger transitions automatically. This is the state pattern working as intended.

State Transitions and Finite State Machines

The state design pattern is the object-oriented implementation of a finite state machine (FSM). An FSM defines a fixed set of states, a set of events (or inputs), and the transitions between states that those events trigger. Every system built with the state pattern is an FSM at its core -- the states are your concrete state classes, the events are the methods on the state interface, and the transitions are the SetState calls inside each concrete state.

Understanding this connection helps you design state-based systems more deliberately. Before writing code, sketch out your states and transitions. A simple table works well:

Current State Event Next State Action
Pending Confirm Confirmed Validate payment info
Pending Cancel Cancelled Release held inventory
Confirmed Ship Shipped Generate tracking number
Confirmed Cancel Cancelled Issue refund
Shipped Deliver Delivered Send confirmation email

This table is your FSM definition. Each row maps directly to a method implementation in a concrete state class. The "Action" column becomes the logic inside that method before the SetState call. Building the table first helps you catch missing transitions, invalid paths, and ambiguous states before you write any code.

One design decision worth calling out: who owns the transition logic? In our implementation, the concrete states decide the next state by calling SetState directly. This is the most common approach and works well when transitions are tightly coupled to state-specific behavior. An alternative is to centralize transitions in the context or in a separate transition table. Centralizing makes the FSM definition easier to see at a glance but can lead to a large conditional block in the context -- exactly what the state pattern is meant to avoid. For most scenarios, keeping transitions in the concrete states is the better trade-off.

State Pattern vs. Strategy Pattern

The state pattern and the strategy design pattern share the same structural skeleton -- both use composition and an interface to delegate behavior. A context holds a reference to an interface, and concrete implementations of that interface define the behavior. If you look at the class diagrams, they're nearly identical. But the intent and usage are fundamentally different.

The strategy pattern is about selecting an algorithm at runtime. The client typically chooses the strategy explicitly, and the strategy doesn't change itself. A sorting algorithm, a compression method, a pricing calculator -- these are strategy use cases. The strategy doesn't know about other strategies or trigger transitions between them.

The state pattern is about an object whose behavior changes as its internal condition changes. The states are aware of each other because they trigger transitions. A state object often creates and sets the next state directly. The client doesn't choose the state -- the system transitions through states based on events and rules.

Here's a quick heuristic: if the behavior swap is driven by the client and happens once (or infrequently), it's probably a strategy. If the behavior swap is driven by the object's own lifecycle and happens as a result of actions taken on the object, it's a state. Both patterns can benefit from dependency injection for managing the creation and wiring of the concrete implementations.

Combining with Other Patterns

The pattern integrates naturally with several other patterns, and knowing these combinations helps you build more robust systems.

The observer design pattern pairs well with the state pattern when other parts of the system need to react to state transitions. The context can raise events when its state changes, letting observers update UI elements, trigger logging, send notifications, or synchronize dependent components. This is especially useful in applications with dashboards or audit requirements where external systems track state changes.

The command design pattern can wrap state-triggering actions as command objects. This lets you queue state transitions, implement undo for state changes, or log every action that caused a transition. The combination is powerful in workflow engines where you need both state management and an audit trail.

The adapter design pattern is useful when you need to integrate a third-party component as a state. If an external library exposes behavior that maps to one of your states but doesn't implement your state interface, an adapter lets you bridge the gap without modifying the library code.

Benefits and Drawbacks

The state pattern offers clear advantages, but it introduces trade-offs you should weigh before adopting it.

Benefits:

  • Eliminates conditional complexity. Large switch statements and nested if-else chains get replaced by polymorphism. Each state class handles its own logic, making the codebase cleaner and easier to navigate.
  • Open/Closed Principle. Adding a new state means creating a new class. You don't modify the context or existing states (unless you need to add transitions to them).
  • Single Responsibility Principle. Each concrete state class focuses on the behavior for one condition. No class is overloaded with logic for multiple states.
  • Explicit state model. The states and transitions are visible as classes and method calls, making the system's behavior easier to understand, document, and test.
  • Testability. You can test each state in isolation by instantiating it directly and calling its methods with a mock or stub context.

Drawbacks:

  • Class proliferation. Each state requires its own class. For a system with many states, this can lead to a large number of small classes that are individually simple but collectively add navigational overhead to the codebase.
  • Tight coupling between states. Concrete states often reference each other directly when triggering transitions. This creates dependencies between state classes that can make refactoring transitions more involved.
  • Overhead for simple cases. If your object has only two or three states with simple logic, the state pattern may be overkill. A straightforward enum and a small switch statement can be simpler and more readable for trivial cases.
  • Transition logic spread. When transitions are distributed across concrete states, it can be harder to see the full FSM at a glance. You need to look at every state class to understand the complete set of valid transitions.

Frequently Asked Questions

What is the state design pattern in C#?

The state design pattern in C# is a behavioral design pattern that allows an object to change its behavior when its internal state changes. You define a state interface, create concrete classes for each state, and let the context object delegate behavior to the active state. This eliminates conditional logic and makes it straightforward to add new states by implementing the interface. In C#, the pattern typically involves an IState interface with methods for each action, sealed concrete state classes, and a context class that holds a reference to the current state.

When should I use the state design pattern?

Use the state pattern when an object's behavior depends on its state and it must change behavior at runtime based on that state. It's especially valuable when you find yourself writing large switch statements or nested if-else chains that check a status field before deciding what to do. Common use cases include order processing workflows, document approval pipelines, game character states, UI component modes, and connection handling (connecting, connected, disconnected). If your object has only two simple states, a basic enum with a switch might be simpler.

How does the state pattern eliminate conditional logic?

Instead of a single method containing a switch or if-else chain that checks the current state and branches accordingly, the state pattern moves each branch into its own class. The context delegates to the current state object, and polymorphism handles the dispatch. When you call order.Confirm(), the context calls _currentState.Confirm(this), and the active state class determines what happens. No conditional check is needed because the right behavior is already encapsulated in the right class.

What is the difference between the state pattern and the strategy pattern?

Both patterns use composition and interfaces to delegate behavior, and their class diagrams look almost identical. The difference is in intent and transition ownership. The strategy pattern lets the client select an algorithm at runtime -- the strategy doesn't change itself. The state pattern lets an object transition between states automatically based on actions and rules -- the states drive the transitions. States know about other states and trigger transitions between them. Strategies are typically stateless and interchangeable without awareness of each other. You can learn more about the strategy pattern and how it differs structurally.

Can I use enums instead of the state pattern?

Yes, for simple cases. An enum combined with a switch statement works fine when you have a small number of states with minimal behavior differences. But as the number of states and actions grows, the switch approach becomes hard to maintain -- every new state requires changes to every switch block, and the logic for all states lives in one place. The state pattern distributes behavior across classes, which scales better as complexity increases. If you're modifying a switch in multiple methods every time you add a state, it's time to refactor to the state pattern.

How do I handle shared behavior across states?

When multiple states share common behavior for certain actions, use an abstract base class instead of (or alongside) an interface. The base class can provide default implementations for methods that behave the same across most states, and concrete states override only the methods where their behavior differs. This reduces duplication without sacrificing the benefits of the pattern. In C#, you can also use default interface methods (introduced in C# 8) to provide fallback behavior directly in the interface, though abstract base classes are more conventional for this purpose.

How does the state pattern relate to finite state machines?

The state design pattern is the object-oriented implementation of a finite state machine. An FSM defines a fixed set of states, a set of inputs (events), and rules for transitioning between states based on those inputs. In the pattern, concrete state classes represent the FSM's states, methods on the state interface represent events, and the SetState calls inside concrete states define transitions. Before implementing the state pattern, it's helpful to define your FSM as a transition table -- listing every state, event, and resulting state -- so you can verify completeness and catch missing or invalid transitions before writing code.

Wrapping Up the State Design Pattern in C#

The state design pattern in C# is a powerful behavioral pattern that replaces conditional complexity with clean, polymorphic delegation. Each state gets its own class, the context delegates to the active state, and transitions happen naturally as actions are taken on the object. The result is code that's easier to read, test, and extend -- especially as the number of states grows.

Start by identifying places in your codebase where an object's behavior depends on its condition -- look for switch statements that check a status field, or if-else chains that branch based on an enum value. If those conditionals span multiple methods and keep growing every time you add a new status, the state design pattern gives you a structured way to decompose that logic. Pair it with the observer pattern to broadcast state transitions, or with the command pattern to queue and undo state-triggering actions. You can find the state pattern alongside other behavioral and structural patterns in the complete list of design patterns.

Delta State Algorithm Creation Series

Part 1 - Exploring Graphs and Trees

Strategy vs State Pattern in C#: Key Differences Explained

Strategy vs State pattern in C#: key differences, when to use each behavioral pattern, and implementation examples to help you choose the right pattern.

An error has occurred. This application may no longer respond until reloaded. Reload