How to Implement State Pattern in C#: Step-by-Step Guide
Changing an object's behavior based on its internal state is one of the most common challenges in application development. The state pattern is a behavioral design pattern that lets an object alter its behavior when its internal state changes, making it appear as though the object changes its class. If you want to implement state pattern in C#, this guide walks you through the entire process from defining the state interface to wiring everything into a dependency injection container. By the end, you'll have complete, working code for an order processing state machine that handles real-world transitions, guard conditions, and clean separation of concerns.
We'll build progressively -- starting with a minimal state interface, then adding concrete states, a context class, transition logic, guard conditions, and finally DI registration. Each step includes complete C# code you can compile and adapt to your own projects.
Prerequisites
Before getting started, make sure you're familiar with these fundamentals:
- C# interfaces and classes: You'll define a state interface and implement multiple concrete state classes. Understanding how interfaces enforce contracts is essential.
- Composition over inheritance: The context object delegates behavior to a state object it holds through composition. This keeps each state isolated from the others.
- Dependency injection basics: The final step covers registering the context and states with IServiceCollection. Familiarity with service registration will help.
- .NET 8 or later: The code examples use modern C# syntax. Any recent .NET SDK works.
Step 1: Define the IState Interface
The first step to implement state pattern in C# is defining the state interface. This interface declares the operations that every state must support. Each method on the interface represents an action that the context can perform -- but the behavior of that action changes depending on which state is at that moment active.
public interface IOrderState
{
string StatusName { get; }
void Submit(OrderContext context);
void Pay(OrderContext context);
void Ship(OrderContext context);
void Deliver(OrderContext context);
void Cancel(OrderContext context);
}
The interface accepts the OrderContext as a parameter on every method. This is critical. Each concrete state needs a reference to the context so it can trigger state transitions -- calling context.TransitionTo(new SomeOtherState()) when conditions are met. Without this back-reference, states would have no way to advance the workflow.
Notice that every possible action appears on the interface, even though most states will only handle one or two of them meaningfully. A draft order can be submitted but not shipped. A delivered order can't be submitted again. The interface covers all actions, and each concrete state decides which ones it supports and which ones it rejects. This is a deliberate design choice -- the context delegates every call to the current state, and the state handles acceptance or rejection.
This approach aligns with the principle of inversion of control. The context depends on the IOrderState abstraction, never on the concrete state classes. That separation lets you add new states without modifying the context.
Step 2: Create Concrete States
Now we build the concrete state classes. Each one implements IOrderState and defines the behavior for a specific phase of the order lifecycle. This is the step where you really implement state pattern in C# -- each state encapsulates the rules and actions for a single point in the workflow.
We need a base class first to provide default "invalid operation" behavior. This avoids repeating rejection logic across every state:
public abstract class OrderStateBase : IOrderState
{
public abstract string StatusName { get; }
public virtual void Submit(OrderContext context)
{
Console.WriteLine(
$"[{StatusName}] Cannot submit order " +
$"in current state.");
}
public virtual void Pay(OrderContext context)
{
Console.WriteLine(
$"[{StatusName}] Cannot process payment " +
$"in current state.");
}
public virtual void Ship(OrderContext context)
{
Console.WriteLine(
$"[{StatusName}] Cannot ship order " +
$"in current state.");
}
public virtual void Deliver(OrderContext context)
{
Console.WriteLine(
$"[{StatusName}] Cannot deliver order " +
$"in current state.");
}
public virtual void Cancel(OrderContext context)
{
Console.WriteLine(
$"[{StatusName}] Cannot cancel order " +
$"in current state.");
}
}
The base class rejects every action by default. Concrete states override only the methods that are valid transitions for their stage. This eliminates boilerplate and makes it obvious which transitions each state supports.
DraftState
public sealed class DraftState : OrderStateBase
{
public override string StatusName => "Draft";
public override void Submit(OrderContext context)
{
Console.WriteLine(
"[Draft] Order submitted for payment.");
context.TransitionTo(new PendingPaymentState());
}
public override void Cancel(OrderContext context)
{
Console.WriteLine(
"[Draft] Order cancelled.");
context.TransitionTo(new CancelledState());
}
}
PendingPaymentState
public sealed class PendingPaymentState : OrderStateBase
{
public override string StatusName => "PendingPayment";
public override void Pay(OrderContext context)
{
Console.WriteLine(
"[PendingPayment] Payment received. " +
"Order is now processing.");
context.TransitionTo(new ProcessingState());
}
public override void Cancel(OrderContext context)
{
Console.WriteLine(
"[PendingPayment] Order cancelled " +
"before payment completed.");
context.TransitionTo(new CancelledState());
}
}
ProcessingState
public sealed class ProcessingState : OrderStateBase
{
public override string StatusName => "Processing";
public override void Ship(OrderContext context)
{
Console.WriteLine(
"[Processing] Order shipped.");
context.TransitionTo(new ShippedState());
}
public override void Cancel(OrderContext context)
{
Console.WriteLine(
"[Processing] Order cancelled. " +
"Initiating refund.");
context.TransitionTo(new CancelledState());
}
}
ShippedState
public sealed class ShippedState : OrderStateBase
{
public override string StatusName => "Shipped";
public override void Deliver(OrderContext context)
{
Console.WriteLine(
"[Shipped] Order delivered successfully.");
context.TransitionTo(new DeliveredState());
}
}
DeliveredState and CancelledState
public sealed class DeliveredState : OrderStateBase
{
public override string StatusName => "Delivered";
}
public sealed class CancelledState : OrderStateBase
{
public override string StatusName => "Cancelled";
}
A few things to notice about these states. DeliveredState and CancelledState are terminal -- they override nothing, so every action is rejected. DraftState only allows Submit and Cancel. Each state controls exactly which transitions are valid. This is the core strength when you implement state pattern in C#: the rules for each state live inside that state, not scattered across conditional blocks in the context.
This structure maps closely to how the strategy design pattern encapsulates algorithms behind interfaces. The key difference is lifecycle: strategies are typically swapped by external code, while states transition themselves as part of internal workflow logic.
Step 3: Build the Context Class
The context class is the public-facing object that clients interact with. When you implement state pattern in C#, the context holds a reference to the current state and delegates all behavior to it. The context itself contains no conditional logic about what to do in each state -- that's entirely the state's responsibility.
public sealed class OrderContext
{
public IOrderState CurrentState { get; private set; }
public string OrderId { get; }
public decimal TotalAmount { get; }
public bool HasShippingAddress { get; set; }
public OrderContext(
string orderId,
decimal totalAmount)
{
OrderId = orderId;
TotalAmount = totalAmount;
CurrentState = new DraftState();
Console.WriteLine(
$"[Order {OrderId}] Created with " +
$"total ${TotalAmount}. " +
$"State: {CurrentState.StatusName}");
}
public void TransitionTo(IOrderState newState)
{
Console.WriteLine(
$"[Order {OrderId}] Transitioning: " +
$"{CurrentState.StatusName} -> " +
$"{newState.StatusName}");
CurrentState = newState;
}
public void Submit() => CurrentState.Submit(this);
public void Pay() => CurrentState.Pay(this);
public void Ship() => CurrentState.Ship(this);
public void Deliver() => CurrentState.Deliver(this);
public void Cancel() => CurrentState.Cancel(this);
}
The context exposes clean public methods -- Submit, Pay, Ship, Deliver, Cancel -- that mirror the state interface. Each one delegates to the current state, passing this as the context reference. That's the entire wiring mechanism. Client code calls order.Ship(), and the current state decides what happens.
The TransitionTo method is intentionally simple. It swaps the current state and logs the change. Some implementations make this method internal so only states within the same assembly can call it -- that's a valid approach to prevent external code from forcing invalid transitions.
Here's a complete example showing the order lifecycle:
var order = new OrderContext("ORD-001", 99.99m);
order.Submit(); // Draft -> PendingPayment
order.Pay(); // PendingPayment -> Processing
order.Ship(); // Processing -> Shipped
order.Deliver(); // Shipped -> Delivered
order.Cancel(); // Rejected -- already delivered
This produces output tracing the entire state machine flow. Each transition is handled by the state that's active at the time. The context never checks "am I in the shipped state?" -- it just asks the current state to handle the action.
Step 4: Implement State Transitions
At this point, the basic transitions work, but production state machines need validation and event handling around transitions. Adding transition events lets other parts of your system react to state changes -- logging, notifications, metrics, and audit trails all benefit from a centralized transition hook.
Add transition event support to the context:
public sealed class OrderContext
{
public event EventHandler<StateTransitionEventArgs>?
StateChanged;
public IOrderState CurrentState { get; private set; }
public string OrderId { get; }
public decimal TotalAmount { get; }
public bool HasShippingAddress { get; set; }
public OrderContext(
string orderId,
decimal totalAmount)
{
OrderId = orderId;
TotalAmount = totalAmount;
CurrentState = new DraftState();
}
public void TransitionTo(IOrderState newState)
{
var previousState = CurrentState;
CurrentState = newState;
Console.WriteLine(
$"[Order {OrderId}] " +
$"{previousState.StatusName} -> " +
$"{newState.StatusName}");
StateChanged?.Invoke(
this,
new StateTransitionEventArgs(
previousState.StatusName,
newState.StatusName));
}
public void Submit() => CurrentState.Submit(this);
public void Pay() => CurrentState.Pay(this);
public void Ship() => CurrentState.Ship(this);
public void Deliver() => CurrentState.Deliver(this);
public void Cancel() => CurrentState.Cancel(this);
}
public sealed class StateTransitionEventArgs : EventArgs
{
public string FromState { get; }
public string ToState { get; }
public StateTransitionEventArgs(
string fromState,
string toState)
{
FromState = fromState;
ToState = toState;
}
}
Now external code can subscribe to transitions without coupling to the states themselves:
var order = new OrderContext("ORD-002", 149.50m);
order.StateChanged += (sender, args) =>
{
Console.WriteLine(
$"[Audit] Order transitioned from " +
$"{args.FromState} to {args.ToState}");
};
order.Submit();
order.Pay();
order.Ship();
order.Deliver();
This event-based approach is essential for implementing the state pattern in production code. Subscribers react to changes without the context knowing who's listening. For more sophisticated pub/sub scenarios, you might reach for something like the command design pattern to encapsulate each transition as a replayable operation.
Step 5: Add Guard Conditions
Real-world state machines don't just check "is this transition allowed?" -- they also validate business rules before transitioning. When you implement state pattern in C#, guard conditions let you enforce constraints like "an order can't ship without a shipping address" or "only orders above $50 qualify for express processing."
Update ProcessingState to include guard conditions:
public sealed class ProcessingState : OrderStateBase
{
public override string StatusName => "Processing";
public override void Ship(OrderContext context)
{
if (!context.HasShippingAddress)
{
Console.WriteLine(
"[Processing] Cannot ship -- " +
"no shipping address on file.");
return;
}
if (context.TotalAmount <= 0)
{
Console.WriteLine(
"[Processing] Cannot ship -- " +
"invalid order total.");
return;
}
Console.WriteLine(
"[Processing] Guard conditions passed. " +
"Order shipped.");
context.TransitionTo(new ShippedState());
}
public override void Cancel(OrderContext context)
{
Console.WriteLine(
"[Processing] Order cancelled. " +
"Initiating refund.");
context.TransitionTo(new CancelledState());
}
}
Here's an example showing the guard in action:
var order = new OrderContext("ORD-003", 75.00m);
order.HasShippingAddress = false;
order.Submit();
order.Pay();
order.Ship();
// Output: [Processing] Cannot ship --
// no shipping address on file.
order.HasShippingAddress = true;
order.Ship();
// Output: [Processing] Guard conditions passed.
// Order shipped.
The guard condition prevents the transition until the business rule is satisfied. The state itself decides whether to proceed -- the context doesn't need to know what validations are required. This is a clean pattern because adding a new guard means editing only the relevant state class. No switch statements, no cascading if-else chains.
You can extract guard logic into its own class for reusability if validations get complex:
public sealed class ShippingGuard
{
public bool CanShip(
OrderContext context,
out string reason)
{
if (!context.HasShippingAddress)
{
reason = "No shipping address on file.";
return false;
}
if (context.TotalAmount <= 0)
{
reason = "Invalid order total.";
return false;
}
reason = string.Empty;
return true;
}
}
This separation is similar to how the adapter design pattern wraps existing logic behind a clean interface. The guard encapsulates validation rules that the state can call without embedding those rules directly in the transition method.
Step 6: Wire Up with Dependency Injection
For production applications, you'll want to register your context and supporting services with a DI container. When you implement state pattern in C#, the context is typically a transient or scoped service -- each order gets its own context -- while guards and shared services might be singletons.
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<ShippingGuard>();
services.AddTransient<Func<string, decimal, OrderContext>>(
sp => (orderId, totalAmount) =>
{
var context = new OrderContext(
orderId,
totalAmount);
return context;
});
var provider = services.BuildServiceProvider();
var createOrder = provider
.GetRequiredService<Func<string, decimal, OrderContext>>();
var order = createOrder("ORD-DI-001", 250.00m);
order.HasShippingAddress = true;
order.Submit();
order.Pay();
order.Ship();
order.Deliver();
Console.WriteLine(
$"Final state: {order.CurrentState.StatusName}");
We register a factory delegate rather than the OrderContext itself because each order requires unique parameters -- an order ID and a total amount. The factory captures the service provider so it can resolve any dependencies the context might need.
For more advanced scenarios, you can inject guards into states through a state factory:
public interface IOrderStateFactory
{
IOrderState CreateDraft();
IOrderState CreateProcessing(
ShippingGuard shippingGuard);
}
public sealed class OrderStateFactory
: IOrderStateFactory
{
private readonly ShippingGuard _shippingGuard;
public OrderStateFactory(ShippingGuard shippingGuard)
{
_shippingGuard = shippingGuard;
}
public IOrderState CreateDraft()
{
return new DraftState();
}
public IOrderState CreateProcessing(
ShippingGuard shippingGuard)
{
return new ProcessingState();
}
}
Register the factory and use it:
services.AddSingleton<IOrderStateFactory,
OrderStateFactory>();
This factory approach keeps state creation centralized. The context or transition logic asks the factory for the next state rather than calling new directly. That makes testing easier because you can substitute a mock factory that returns test-specific states. The consuming code depends on the factory abstraction, not concrete state types -- following the same principles covered in inversion of control.
Common Mistakes to Avoid
Even experienced developers make these mistakes when they first implement state pattern in C#.
Putting transition logic in the context class: The whole point of the state pattern is to push state-specific behavior into state objects. If your context class has switch statements or if-else chains checking the current state, you've missed the pattern's benefit. Every action should delegate to the current state.
Allowing invalid state transitions silently: If DraftState.Ship() does nothing and prints no message, you'll struggle to debug workflow issues. Always provide clear feedback when an action is rejected. Throw an exception or log a meaningful message so invalid transitions are visible.
Creating circular state dependencies: If StateA creates StateB and StateB creates StateA through direct new calls, you've created tight coupling between states. Use a factory to create states, or at minimum ensure states only reference the state interface, not concrete classes they transition to.
Bloating the state interface: Every method on IOrderState must be implemented by every concrete state. If you add a Refund method, all six state classes need to address it -- even if only one state supports refunds. Keep the interface focused on the core actions of your workflow. For rare operations, consider a separate interface or a method on the context.
Ignoring thread safety: If multiple threads can call methods on the same context, state transitions can race. One thread might check the current state while another is mid-transition. For thread-safe state machines, synchronize access to TransitionTo or use an immutable state pattern where each transition returns a new context rather than mutating the existing one.
Frequently Asked Questions
What is the state pattern and why should I use it in C#?
The state pattern is a behavioral design pattern that allows an object to change its behavior based on its internal state. Instead of using large conditional blocks to determine what an object should do, you encapsulate each state's behavior in its own class. You should implement state pattern in C# when your object has distinct modes of operation with different rules in each mode -- order processing, document workflows, game character states, and connection management are all classic examples. The pattern makes adding new states straightforward because each state is an independent class with no impact on existing states.
How does the state pattern differ from the strategy pattern?
Both patterns use composition and interface-based delegation, but their intent is fundamentally different. The strategy pattern lets external code swap algorithms at runtime -- the caller picks the strategy. When you implement state pattern in C#, the object transitions itself between states based on internal logic. States know about each other and manage their own transitions. Strategies are interchangeable and don't know about each other. Think of strategies as "choose how to do it" and states as "the object knows what it is."
Can the state pattern handle complex transition rules?
Yes. Guard conditions let you add business rule validation before any transition occurs. Each concrete state can check arbitrary conditions -- data completeness, user permissions, time-based rules -- before allowing the transition to proceed. You can extract complex guard logic into dedicated validator classes that states call during their transition methods. This keeps individual state classes focused while still supporting intricate business requirements.
How do I test state pattern implementations in C#?
Test each concrete state in isolation by creating a context, setting it to the state under test, and calling each method. Verify that valid actions trigger the expected transition and that invalid actions are properly rejected. For example, create an OrderContext in ProcessingState, call Ship() with and without a shipping address, and assert the resulting state. You can also test the full workflow end-to-end by running through the complete lifecycle and verifying each intermediate state. Mocking the context with an interface makes unit testing even cleaner.
Should I use enums or the state pattern for state machines?
Enums work well for simple state machines with few states and straightforward transitions. If your state logic is a single switch statement with a handful of cases, an enum is simpler and more readable. However, when each state has meaningfully different behavior -- different validations, different side effects, different allowed actions -- the state pattern scales much better. When you implement state pattern in C#, adding a new state means adding a new class. With enums, adding a new state means updating every switch statement that references the enum, which violates the open-closed principle.
How does the state pattern work with dependency injection?
The context is typically registered as a transient or scoped service, since each workflow instance needs its own state. Shared services like validators and guards are registered as singletons. Since states are short-lived objects created during transitions, they aren't registered directly in the container. Instead, use a state factory that receives its dependencies through dependency injection and creates state instances on demand. The factory resolves any services the states need, keeping the states themselves free from direct container coupling.
Can I combine the state pattern with other design patterns?
Absolutely. The state pattern pairs well with several other patterns. Use the decorator design pattern to wrap states with logging, metrics, or authorization checks without modifying the states themselves. The command design pattern can encapsulate state transitions as replayable operations for audit trails or undo support. And a state factory naturally emerges when you need to inject dependencies into states, keeping construction logic centralized and testable.

