State Pattern in C#: Complete Developer Guide with Examples and Best Practices (2025)

State Pattern

The State pattern is one of those design patterns that can make you feel like a coding wizard when you finally “get it.” But let’s be honest – it’s also one of the most misunderstood patterns, especially for developers who are still building their design pattern toolkit.

If you’ve ever found yourself drowning in nested if-else statements while trying to handle different behaviors based on an object’s state, or wondered why your colleague keeps mentioning “state machines” in code reviews, this guide is for you.

Table of Contents

What Is the State Pattern and Why Should You Care?

The State pattern allows an object to change its behavior when its internal state changes. It appears as if the object changed its class. Think of it like a chameleon – same animal, different colors and behaviors depending on its environment.

In practical terms, the State pattern helps you avoid the dreaded “spaghetti code” that happens when you try to handle multiple states with endless conditional statements. Instead of having one massive class with tons of if-else blocks, you distribute state-specific behavior across separate state classes.

The Real-World Problem It Solves

Imagine you’re building a media player application. Your player can be in different states: playing, paused, stopped, or buffering. Each state requires different behavior when the user clicks “play,” “pause,” or “stop.”

Without the State pattern, you might end up with something like this mess:

				
					public class MediaPlayer
{
    private PlayerState currentState;
    
    public void Play()
    {
        if (currentState == PlayerState.Stopped)
        {
            // Start playing from beginning
        }
        else if (currentState == PlayerState.Paused)
        {
            // Resume playing
        }
        else if (currentState == PlayerState.Playing)
        {
            // Already playing, do nothing
        }
        else if (currentState == PlayerState.Buffering)
        {
            // Queue play request
        }
        // ... and it gets worse with more states
    }
}
				
			

This approach becomes unmaintainable as you add more states and behaviors. The State pattern gives you a cleaner, more organized solution.

When to Use the State Pattern (And When NOT To)

Perfect Scenarios for State Pattern

Complex State Machines: When you have an object with multiple states and complex transitions between them. Examples include game characters, workflow systems, or user interface components.

Behavior Varies Significantly by State: If the same method needs to do completely different things based on the current state, the State pattern shines.

Frequent State Changes: When your object changes states often during its lifetime, having dedicated state classes makes the code much more maintainable.

Growing Complexity: If you find yourself adding more and more conditional logic to handle different states, it’s time to refactor using the State pattern.

When to Avoid the State Pattern

Simple Boolean States: If you only have two states (like enabled/disabled), a simple boolean flag is often sufficient.

Rare State Changes: If your object rarely changes state, the additional complexity might not be worth it.

No Behavioral Differences: If different states don’t require different behaviors, you probably don’t need the State pattern.

The Anatomy of the State Pattern

The State pattern consists of three main components:

State Pattern UML Diagram
1. The State Interface

This defines the contract that all concrete states must follow. It typically includes methods that represent the actions that can be performed in any state.

2. Concrete State Classes

These implement the State interface and contain the specific behavior for each state. Each state knows how to handle the actions defined in the interface.

3. The Context Class

This is the class whose behavior changes based on its state. It maintains a reference to the current state and delegates state-specific behavior to the current state object.

Building Your First State Pattern Implementation

Let’s build a practical example: a simple document editor that can be in different states (editing, read-only, and reviewing). Each state handles user actions differently.

Step 1: Define the State Interface
				
					public interface IDocumentState
{
    void Edit(DocumentContext context, string content);
    void Save(DocumentContext context);
    void SetReadOnly(DocumentContext context);
    void StartReview(DocumentContext context);
}
				
			
Step 2: Create the Context Class
				
					public class DocumentContext
{
    private IDocumentState currentState;
    public string Content { get; set; }
    public List<string> Comments { get; set; }
    
    public DocumentContext()
    {
        Comments = new List<string>();
        Content = "";
        currentState = new EditingState();
    }
    
    public void SetState(IDocumentState newState)
    {
        currentState = newState;
    }
    
    public void Edit(string content) => currentState.Edit(this, content);
    public void Save() => currentState.Save(this);
    public void SetReadOnly() => currentState.SetReadOnly(this);
    public void StartReview() => currentState.StartReview(this);
}
				
			
Step 3: Implement Concrete States
				
					public class EditingState : IDocumentState
{
    public void Edit(DocumentContext context, string content)
    {
        context.Content = content;
        Console.WriteLine("Content updated successfully");
    }
    
    public void Save(DocumentContext context)
    {
        Console.WriteLine("Document saved");
    }
    
    public void SetReadOnly(DocumentContext context)
    {
        context.SetState(new ReadOnlyState());
        Console.WriteLine("Document is now read-only");
    }
    
    public void StartReview(DocumentContext context)
    {
        context.SetState(new ReviewingState());
        Console.WriteLine("Document is now in review mode");
    }
}

public class ReadOnlyState : IDocumentState
{
    public void Edit(DocumentContext context, string content)
    {
        Console.WriteLine("Cannot edit: Document is read-only");
    }
    
    public void Save(DocumentContext context)
    {
        Console.WriteLine("Cannot save: Document is read-only");
    }
    
    public void SetReadOnly(DocumentContext context)
    {
        Console.WriteLine("Document is already read-only");
    }
    
    public void StartReview(DocumentContext context)
    {
        Console.WriteLine("Cannot review: Document is read-only");
    }
}
				
			

This implementation demonstrates how each state handles the same operations differently, making the code much more organized and maintainable.

Common Pitfalls and How to Avoid Them

Pitfall 1: Confusing State with Strategy Pattern

Many developers mix up the State and Strategy patterns because they have similar structures. Here’s the key difference:

  • State Pattern: The object’s behavior changes based on its internal state, and states often know about each other
  • Strategy Pattern: The behavior is chosen externally and strategies are typically independent

Remember: State is about “what I am,” Strategy is about “how I do it.”

Pitfall 2: States Knowing Too Much About Each Other

A common mistake is making states directly dependent on each other. Instead of having states directly reference other states, let the context handle state transitions:

				
					// Bad: State directly changes to another state
public void Complete(DocumentContext context)
{
    context.SetState(new CompletedState()); // State controls transition
}

// Better: Context handles transition logic
public void Complete(DocumentContext context)
{
    context.TransitionToCompleted(); // Context controls transition
}
				
			

Pitfall 3: Over-Engineering Simple Scenarios

Don’t use the State pattern for simple boolean states. If you only have two states with minimal behavioral differences, a simple flag might be more appropriate:

				
					// Overkill for simple on/off behavior
public class LightState { }
public class OnState : LightState { }
public class OffState : LightState { }

// Better for simple scenarios
public class Light
{
    private bool isOn;
    public void Toggle() => isOn = !isOn;
}
				
			

Pitfall 4: Forgetting State Initialization

Always ensure your context starts with a valid state. Null reference exceptions are common when developers forget to initialize the starting state:

				
					public class DocumentContext
{
    public DocumentContext()
    {
        currentState = new EditingState(); // Always initialize!
    }
}
				
			

Advanced State Pattern Techniques

State Transition Validation

In complex systems, you might want to validate state transitions. Not all states should be able to transition to any other state:

				
					public class DocumentContext
{
    private readonly Dictionary<Type, List<Type>> validTransitions;
    
    public DocumentContext()
    {
        validTransitions = new Dictionary<Type, List<Type>>
        {
            { typeof(EditingState), new List<Type> { typeof(ReadOnlyState), typeof(ReviewingState) } },
            { typeof(ReadOnlyState), new List<Type> { typeof(EditingState) } },
            { typeof(ReviewingState), new List<Type> { typeof(EditingState) } }
        };
    }
    
    public bool CanTransitionTo<T>() where T : IDocumentState
    {
        return validTransitions[currentState.GetType()].Contains(typeof(T));
    }
}
				
			

State History and Undo Operations

For applications that need undo functionality, you can maintain a history of states:

				
					public class DocumentContext
{
    private Stack<IDocumentState> stateHistory;
    
    public void SetState(IDocumentState newState)
    {
        stateHistory.Push(currentState);
        currentState = newState;
    }
    
    public void Undo()
    {
        if (stateHistory.Count > 0)
        {
            currentState = stateHistory.Pop();
        }
    }
}
				
			

Testing Your State Pattern Implementation

Testing state-based systems requires a different approach. Focus on testing state transitions and state-specific behaviors:

				
					[Test]
public void EditingState_ShouldTransitionToReadOnly()
{
    var document = new DocumentContext();
    
    document.SetReadOnly();
    
    // Verify state changed by testing behavior
    document.Edit("New content");
    Assert.That(document.Content, Is.Empty); // Should not change in read-only
}
				
			

Test each state independently and verify that transitions work correctly. Mock the context when testing individual states to ensure they’re properly isolated.

Performance Considerations

The State pattern can introduce some performance overhead due to object creation and method delegation. Here are some optimization strategies:

State Reuse

Instead of creating new state objects for each transition, consider reusing stateless state objects:

				
					public class DocumentContext
{
    private static readonly IDocumentState editingState = new EditingState();
    private static readonly IDocumentState readOnlyState = new ReadOnlyState();
    
    public void SetToEditing()
    {
        currentState = editingState;
    }
}
				
			

Lazy State Creation

For complex states that are expensive to create, consider lazy initialization:

Add Your Heading Text Here

				
					private readonly Lazy<IDocumentState> heavyState = 
    new Lazy<IDocumentState>(() => new HeavyState());
				
			

Integration with Other Patterns

The State pattern works well with other design patterns:

Observer Pattern

Notify observers when state changes occur:

				
					public class DocumentContext
{
    public event Action<IDocumentState> StateChanged;
    
    public void SetState(IDocumentState newState)
    {
        currentState = newState;
        StateChanged?.Invoke(newState);
    }
}
				
			

Command Pattern

Use commands to trigger state transitions:

				
					public class SetReadOnlyCommand : ICommand
{
    private readonly DocumentContext document;
    
    public void Execute()
    {
        document.SetReadOnly();
    }
}
				
			

Real-World Applications

The State pattern is particularly useful in:

Game Development: Character states (idle, walking, jumping, attacking) with different behaviors and animations for each state.

Workflow Systems: Document approval processes where documents move through different approval stages.

UI Components: Form validation where the form behaves differently based on its validation state.

Network Protocols: TCP connections with different states (established, listening, closed) that handle packets differently.

E-commerce Systems: Order processing where orders have different states (pending, processing, shipped, delivered) with state-specific operations.

Conclusion: Understanding the State Pattern

The State pattern is a powerful tool for managing complex state-dependent behavior in your applications. It transforms messy conditional logic into clean, maintainable code that’s easy to extend and modify.

Remember the key principles:

  • Use it when you have complex state machines with significant behavioral differences
  • Keep states focused on their specific responsibilities
  • Let the context manage state transitions
  • Don’t over-engineer simple scenarios

Start small with simple examples like the document editor we built, then gradually apply the pattern to more complex scenarios in your projects. With practice, you’ll develop an intuition for when the State pattern is the right choice.

The State pattern isn’t just about writing cleaner code – it’s about modeling your domain more accurately and creating systems that are easier to understand, test, and maintain. Master this pattern, and you’ll find yourself writing more elegant, robust software that can adapt to changing requirements without breaking existing functionality.

Remember: great developers don’t just write code that works – they write code that can evolve. The State pattern is one of your most powerful tools for building adaptable, maintainable software systems.