Pattern: State Type: Behavioral Importance: ⭐⭐⭐ Usage Frequency: Medium
The Problem: Behavior Changes Based on Internal State
Consider a simple document workflow system. A document can be in one of several states: Draft, Published, and Archived. A straightforward implementation might look like this:
public class Document
{
public string State { get; private set; } = "Draft";
public void Publish()
{
if (State == "Draft")
{
Console.WriteLine("Publishing document...");
State = "Published";
}
else if (State == "Published")
{
Console.WriteLine("Document is already published.");
}
else if (State == "Archived")
{
Console.WriteLine("Cannot publish an archived document.");
}
}
public void Archive()
{
if (State == "Published")
{
Console.WriteLine("Archiving document...");
State = "Archived";
}
else
{
Console.WriteLine("Only published documents can be archived.");
}
}
}
This works for a small number of states, but it does not scale well. As more states are introduced:
- The class accumulates conditional logic.
- Each method must handle all possible states.
- Adding a new state requires modifying multiple methods.
- State transitions become harder to reason about.
The underlying issue is that behavior depends on internal state, and that state-dependent logic is centralized in a single class using conditionals. This is the type of scenario the State pattern addresses.
What the State Pattern Is
The State pattern allows an object to change its behavior based on its internal state. Instead of using conditional statements to alter behavior, State moves state-specific behavior into separate classes. The context delegates behavior to the current state object. State is a behavioral pattern. It focuses on organizing state-dependent behavior into separate classes. The essential idea: Encapsulate state-specific behavior in separate classes and delegate calls to the current state. When the state changes, the behavior changes automatically.
Structure Overview
The pattern includes:
- Context – Maintains a reference to the current state and delegates behavior to it.
- State interface – Declares behavior methods that all states must handle.
- Concrete States – Implement state-specific behavior and trigger transitions.
The diagram looks like this:
The context delegates operations to the current state. State objects may change the context’s state.
Refactoring the Example
Before we begin, this pattern has two common implementation approaches — interface-based and abstract-class-based — each with its own strengths. We will implement both, then show the combined approach that many C# developers use in practice.
Interface-Based Implementation
Let’s define the state interface:
public interface IDocumentState
{
void Publish(DocumentContext context);
void Archive(DocumentContext context);
}
After that, the context:
public class DocumentContext
{
private IDocumentState _state;
public DocumentContext()
{
_state = new DraftState();
}
public void SetState(IDocumentState state)
{
_state = state;
}
public void Publish()
{
_state.Publish(this);
}
public void Archive()
{
_state.Archive(this);
}
}
And the concrete states:
public class DraftState : IDocumentState
{
public void Publish(DocumentContext context)
{
Console.WriteLine("Publishing document...");
context.SetState(new PublishedState());
}
public void Archive(DocumentContext context)
{
Console.WriteLine("Cannot archive a draft document.");
}
}
public class PublishedState : IDocumentState
{
public void Publish(DocumentContext context)
{
Console.WriteLine("Document is already published.");
}
public void Archive(DocumentContext context)
{
Console.WriteLine("Archiving document...");
context.SetState(new ArchivedState());
}
}
public class ArchivedState : IDocumentState
{
public void Publish(DocumentContext context)
{
Console.WriteLine("Cannot publish an archived document.");
}
public void Archive(DocumentContext context)
{
Console.WriteLine("Document is already archived.");
}
}
Each state class contains only the behavior relevant to that state.
Abstract Class Implementation
In practice, states almost always share some common logic — logging, validation, default no-op implementations for methods that not every state cares about. An abstract base class solves this:
The abstract state class:
public abstract class DocumentState
{
protected DocumentContext Context { get; private set; }
public void SetContext(DocumentContext ctx)
=> Context = ctx;
// Default: operation not allowed in this state
public virtual void Publish()
=> Log("Cannot publish in this state.");
public virtual void Archive()
=> Log("Cannot archive in this state.");
protected void Log(string msg)
=> Console.WriteLine(
$"[{GetType().Name}] {msg}");
}
This abstract class provides virtual defaults that reject the operation, a shared Log() helper, and a built-in Context reference. States only override what they allow. The context:
public class DocumentContext
{
public DocumentState State { get; private set; }
public DocumentContext()
{
TransitionTo(new DraftState());
}
public void TransitionTo(DocumentState next)
{
Console.WriteLine($"State → {next.GetType().Name}");
State = next;
State.SetContext(this);
}
public void Publish() => State.Publish();
public void Archive() => State.Archive();
}
Concrete states now only override what they allow — everything else falls through to the base class defaults:
public class DraftState : DocumentState
{
public override void Publish()
{
Log("Publishing document...");
Context.TransitionTo(new PublishedState());
}
// Archive() not overridden
// → base class logs "Cannot archive in this state."
}
public class PublishedState : DocumentState
{
public override void Archive()
{
Log("Archiving document...");
Context.TransitionTo(new ArchivedState());
}
// Publish() not overridden
// → base class logs "Cannot publish in this state."
}
public class ArchivedState : DocumentState
{
// Neither method overridden —
// both fall through to base defaults.
// This state is a dead end.
}
The base class handles the boilerplate. ArchivedState is an empty class — all its behavior comes from the defaults.
Combined Approach (Interface + Abstract Base)
The most pragmatic solution provides both an interface for the contract and an abstract base class for convenience: 
// Contract — consumers and tests depend on this
public interface IDocumentState
{
void Publish();
void Archive();
}
// Convenience base — concrete states inherit this
public abstract class DocumentStateBase : IDocumentState
{
protected DocumentContext Context { get; private set; }
public void SetContext(DocumentContext ctx) => Context = ctx;
public virtual void Publish()
=> Log("Not allowed in this state.");
public virtual void Archive()
=> Log("Not allowed in this state.");
protected void Log(string msg)
=> Console.WriteLine($"[{GetType().Name}] {msg}");
}
public class DocumentContext
{
public IDocumentState State { get; private set; }
public void TransitionTo(IDocumentState next)
{
State = next;
if (next is DocumentStateBase baseState)
baseState.SetContext(this);
}
public void Publish() => State.Publish();
public void Archive() => State.Archive();
}
Comparing the Three Approaches
Interface gives decoupling and flexibility. The DocumentContext, unit tests, and any other consuming code depend on IDocumentState — a contract with no baggage. You can mock it trivially in tests, and any class from any hierarchy can become a state just by implementing the two methods. The downside: every state must implement every method, even when the implementation is just “no, you can’t do that.” With three states and two methods it’s manageable, but with ten states and five methods you’d be writing dozens of identical rejection one-liners. Abstract class solves that boilerplate problem. Sensible defaults are defined once (reject the operation and log it), shared helpers like Log() live in one place, and the Context reference is wired automatically. Concrete states only override what they actually permit. ArchivedState becomes an empty class. The trade-off is coupling: everything is locked into a single inheritance chain. You cannot easily mock the base class in tests, and if a state ever needs to inherit from something else, you are stuck. The combined version eliminates the trade-off. Consuming code depends on IDocumentState — full decoupling and testability. Concrete states extend DocumentStateBase and get all the defaults, helpers, and context wiring for free. And the escape hatch is always there: if a state cannot extend the base (because it already inherits from something else), it just implements IDocumentState directly. You do not have to choose between clean contracts and reduced boilerplate — you get both.
Usage
Using the abstract class version:
var document = new DocumentContext(); document.Publish(); document.Archive(); document.Publish();
Output:
State → DraftState [DraftState] Publishing document... State → PublishedState [PublishedState] Archiving document... State → ArchivedState [ArchivedState] Cannot publish in this state.
The document transitions from Draft → Published → Archived. The final Publish() call falls through to the base class default because ArchivedState does not override it. The client interacts only with the context — it does not know which state is active.
What Changed in the Design?
Original implementation:
- State stored as a string or enum.
- Conditional logic scattered across methods.
- Adding a new state required modifying existing logic.
- State transitions were implicit inside conditionals.
Refactored implementation:
- Each state is represented by a class.
- Behavior is localized to state-specific classes.
- Transitions are explicit and centralized.
- The context delegates behavior to the current state.
The architectural improvement is that behavior and transitions are grouped by state rather than by method. This improves readability and extensibility.
When to Use State?
State is appropriate when:
- Object behavior changes based on internal state.
- The class contains large conditional statements depending on state.
- State transitions are complex.
- You want to follow the Open/Closed Principle.
- You want to avoid state-dependent conditionals.
Avoid State when:
- The number of states is small and unlikely to grow — if the behavior differences are just different log messages across 2-3 states, an enum with a switch is clearer.
- State transitions are trivial — if each state only transitions to one other state with no conditional logic, the pattern adds ceremony without benefit.
- The state machine is complex enough to warrant a dedicated state machine library (like Stateless or MassTransit sagas) rather than hand-rolled classes.
State in the .NET Ecosystem
A common real-world example is a connection lifecycle. A database connection may move through states: Closed, Connecting, Open, and Broken. In ADO.NET, DbConnection exposes a State property:
if (connection.State == ConnectionState.Open)
{
// Execute queries
}
else if (connection.State == ConnectionState.Closed)
{
connection.Open();
}
Operations like Open() and Close() behave differently depending on that internal state. While not implemented strictly as GoF State internally (the state is an enum, not a state object), the conceptual model matches: behavior depends on internal state transitions. For more complex state machines in .NET, the Stateless library is a popular choice. It provides a fluent API for defining states, triggers, and transitions:
var machine = new StateMachine<DocumentState, Trigger>(DocumentState.Draft);
machine.Configure(DocumentState.Draft)
.Permit(Trigger.Publish, DocumentState.Published);
machine.Configure(DocumentState.Published)
.Permit(Trigger.Archive, DocumentState.Archived);
This is a declarative alternative to hand-rolled state classes — useful when the state machine is complex enough that managing individual state classes becomes unwieldy.
Common Mistakes
One common mistake is confusing State with Strategy. In Strategy, the client selects and injects the behavior from outside. In State, the object transitions between behaviors internally based on its own state changes. The client does not choose which state to use — the state machine controls that. Another mistake is overusing State when a simple enum would suffice. If the system has three states and the behavior differences amount to different log messages or simple flag checks, an enum with a switch statement is clearer and easier to maintain than three separate classes with a context. A third issue occurs when state classes grow too large and start duplicating logic. This is the signal to introduce a base class with shared defaults (as shown in the abstract class approach) or to extract common behavior into helper methods.
Trade-offs
Let’s see what the advantages and disadvantages of this pattern are: Advantages
- Eliminates complex conditionals State-specific logic is isolated in separate classes.
- Improves extensibility New states can be introduced without modifying existing state classes.
- Makes transitions explicit State changes are clearly defined in each state class.
- Improves readability Each state has a focused responsibility.
Disadvantages
- Increases the number of classes Each state requires its own class.
- Can introduce complexity For small systems, the pattern may be unnecessary.
- Transitions must be carefully designed Poorly structured transitions can lead to difficult debugging.
State vs Similar Patterns
State vs Strategy: State changes behavior based on internal transitions — the object decides its own behavior. Strategy changes behavior based on external configuration — the client selects the algorithm. State vs Template Method: Template Method defines a fixed algorithm structure with overridable steps. State changes behavior dynamically as the object transitions between states.
Potential Interview Questions
What problem does the State pattern solve? It organizes state-dependent behavior into separate classes and eliminates conditional logic. How does State differ from Strategy? State changes behavior internally based on transitions. Strategy is selected externally by the client. When is State preferable to an enum with conditionals? When behavior and transitions are complex or expected to grow, and when each state has significant logic beyond simple flag checks. Who controls transitions in the State pattern? Typically, state classes trigger transitions by updating the context. The client does not choose states directly. What are the trade-offs between interface-based and abstract-class-based State? Interface gives decoupling and testability but requires implementing every method in every state. Abstract class reduces boilerplate with defaults but locks into a single inheritance chain. The combined approach (interface + abstract base) gives both benefits.
Summary
- State encapsulates state-specific behavior in separate classes.
- It replaces conditional logic with polymorphism.
- The context delegates behavior to the current state, and states trigger transitions.
- Three implementation approaches exist: interface (decoupled), abstract class (reduced boilerplate), and combined (both benefits).
- Transitions become explicit and maintainable.
- It is most useful when behavior depends heavily on internal state and the number of states is expected to grow.