BrandGhost
State vs Strategy Pattern in C#: Key Differences Explained

State vs Strategy Pattern in C#: Key Differences Explained

State vs Strategy Pattern in C#: Key Differences Explained

Both the state pattern and the strategy pattern are behavioral design patterns from the Gang of Four catalog, and they share a nearly identical class structure. Both encapsulate behavior behind an interface and delegate work to interchangeable implementations. From a UML diagram, they look the same. But when you examine state vs strategy pattern in C# closely, the behavioral differences are significant -- and choosing the wrong one leads to designs that fight against the problem you're solving.

In this article, we'll break down each pattern's core purpose, walk through side-by-side C# code examples, and provide clear criteria for deciding between state vs strategy. We'll also cover combining both patterns, common mistakes, and a detailed FAQ section. If you want to see how the strategy pattern compares against the command pattern, those dedicated guides go deeper.

Quick Overview: State vs Strategy Foundations

Before diving into the state vs strategy comparison, let's establish what each pattern does on its own.

The State Pattern

The state pattern allows an object to change its behavior when its internal state changes. Internally, the context delegates behavior to a current state object. When conditions change, the context swaps its state object for a different one, and subsequent method calls produce different results.

The defining characteristic is that transitions happen automatically as part of normal operation. The state objects themselves drive the transitions. Each state knows which states it can transition to and under what conditions.

The Strategy Pattern

The strategy pattern encapsulates an algorithm behind an interface and lets clients swap algorithms at runtime. The pattern focuses on how something is done rather than tracking what has happened. Strategies are typically stateless -- they receive input, process it, and return output.

Think of it like choosing a shipping method at checkout. The order stays the same, but the client picks a different strategy -- standard, express, overnight -- depending on priorities.

Side-by-Side State vs Strategy Comparison

Here's a direct comparison of the state vs strategy pattern across the dimensions that matter most in practice.

Intent: Behavior Changes vs Algorithm Selection

The state pattern's intent is to change behavior based on internal state. As the object moves through its lifecycle, different state objects handle the same method calls differently. The state vs strategy distinction starts here -- a state object makes the context behave as if it became a different object entirely.

The strategy pattern's intent is to select an interchangeable algorithm. The client picks a strategy, and that strategy determines how a specific operation is performed. The context doesn't "become" something different -- it just uses a different tool.

Who Controls Transitions

In the state pattern, state objects control transitions. Each state implementation decides when to switch the context to a different state. The client doesn't manage these transitions -- they happen organically as part of the state's logic. When a document moves from Draft to PendingReview, the Draft state object initiates that transition.

In the strategy pattern, the client controls the selection. External code decides which strategy to use and injects it into the context. Strategies never replace themselves -- they have no awareness of the context's configuration. The client code or a dependency injection container chooses which strategy to wire in.

This difference in control is one of the clearest state vs strategy differentiators you'll encounter.

Awareness: States Know Each Other, Strategies Don't

State objects are aware of other states in the system. A "Draft" state knows that "PendingReview" exists because it needs to transition the context to that state. States form a network of transitions, and each state holds references -- directly or indirectly -- to the states it can reach.

Strategy objects are completely independent. A sorting strategy doesn't know that other sorting strategies exist. It doesn't reference them, transition to them, or have any coupling to them. Each strategy is a self-contained algorithm that stands alone.

Lifecycle: Changes Over Time vs Chosen Once

The state pattern is dynamic over the object's lifetime. An order might move through Created, Processing, Shipped, and Delivered states as it progresses. The current state changes repeatedly throughout the object's existence. This ongoing mutation is fundamental to the state vs strategy distinction.

The strategy pattern is typically set once or changed infrequently. You pick a compression algorithm, a validation rule, or a pricing model -- and it stays in effect for the duration of that operation or session. While you can swap strategies at runtime, the pattern doesn't assume or encourage frequent changes the way the state pattern does.

C# Code Examples: State vs Strategy for Document Workflow

Let's make the state vs strategy comparison concrete with a real example. We'll build a document publishing system -- first with the state pattern, then with the strategy pattern -- so you can see how the same domain looks under each approach.

State Pattern Approach

With the state pattern, the document's behavior changes as it moves through workflow stages. Each state controls what operations are valid and triggers transitions to the next state:

public interface IDocumentState
{
    void Edit(DocumentContext context);
    void Submit(DocumentContext context);
    void Approve(DocumentContext context);
    void Reject(DocumentContext context);
    string GetStatusDisplay();
}

public sealed class DocumentContext
{
    public IDocumentState CurrentState { get; private set; }
    public string Content { get; set; } = string.Empty;

    public DocumentContext()
    {
        CurrentState = new DraftState();
    }

    public void SetState(IDocumentState state)
    {
        CurrentState = state;
        Console.WriteLine(
            $"State changed to: {state.GetStatusDisplay()}");
    }

    public void Edit() => CurrentState.Edit(this);
    public void Submit() => CurrentState.Submit(this);
    public void Approve() => CurrentState.Approve(this);
    public void Reject() => CurrentState.Reject(this);
}

Each state implementation handles the same methods differently and drives transitions:

public sealed class DraftState : IDocumentState
{
    public void Edit(DocumentContext context)
    {
        Console.WriteLine("Editing draft content...");
    }

    public void Submit(DocumentContext context)
    {
        Console.WriteLine("Submitting for review...");
        context.SetState(new PendingReviewState());
    }

    public void Approve(DocumentContext context)
    {
        Console.WriteLine(
            "Cannot approve a draft directly.");
    }

    public void Reject(DocumentContext context)
    {
        Console.WriteLine(
            "Cannot reject a draft -- nothing to reject.");
    }

    public string GetStatusDisplay() => "Draft";
}

public sealed class PendingReviewState : IDocumentState
{
    public void Edit(DocumentContext context)
    {
        Console.WriteLine(
            "Cannot edit while pending review.");
    }

    public void Submit(DocumentContext context)
    {
        Console.WriteLine("Already submitted.");
    }

    public void Approve(DocumentContext context)
    {
        Console.WriteLine("Document approved!");
        context.SetState(new PublishedState());
    }

    public void Reject(DocumentContext context)
    {
        Console.WriteLine(
            "Document rejected. Returning to draft.");
        context.SetState(new DraftState());
    }

    public string GetStatusDisplay() => "Pending Review";
}

public sealed class PublishedState : IDocumentState
{
    public void Edit(DocumentContext context)
    {
        Console.WriteLine(
            "Cannot edit a published document.");
    }

    public void Submit(DocumentContext context)
    {
        Console.WriteLine("Already published.");
    }

    public void Approve(DocumentContext context)
    {
        Console.WriteLine("Already approved.");
    }

    public void Reject(DocumentContext context)
    {
        Console.WriteLine(
            "Unpublishing document...");
        context.SetState(new DraftState());
    }

    public string GetStatusDisplay() => "Published";
}

Usage shows the state pattern's defining behavior -- the document changes what it does based on where it is in the workflow:

var doc = new DocumentContext();
doc.Content = "My article content";

doc.Edit();    // "Editing draft content..."
doc.Submit();  // "Submitting for review..."
                // State changed to: Pending Review

doc.Edit();    // "Cannot edit while pending review."
doc.Approve(); // "Document approved!"
                // State changed to: Published

doc.Edit();    // "Cannot edit a published document."

Notice that calling Edit() produces completely different behavior depending on the document's current state. Also notice that transitions happen inside the state objects -- DraftState.Submit() creates a PendingReviewState and sets it on the context. The client code never says "change to pending review." This internal transition control is a core state vs strategy difference.

Strategy Pattern Approach

Now let's solve a related problem in the same domain using the strategy pattern. Instead of modeling workflow transitions, we'll handle document formatting -- choosing how to render the document's content for different output targets:

public interface IDocumentFormatter
{
    string Format(string title, string content);
}

public sealed class HtmlFormatter : IDocumentFormatter
{
    public string Format(string title, string content)
    {
        return $"<h1>{title}</h1>
<p>{content}</p>";
    }
}

public sealed class MarkdownFormatter : IDocumentFormatter
{
    public string Format(string title, string content)
    {
        return $"# {title}

{content}";
    }
}

public sealed class PlainTextFormatter : IDocumentFormatter
{
    public string Format(string title, string content)
    {
        return $"{title.ToUpperInvariant()}
" +
            $"{new string('=', title.Length)}
{content}";
    }
}

The context class accepts any formatter strategy through dependency injection:

public sealed class DocumentPublisher
{
    private IDocumentFormatter _formatter;

    public DocumentPublisher(
        IDocumentFormatter formatter)
    {
        _formatter = formatter;
    }

    public void SetFormatter(
        IDocumentFormatter formatter)
    {
        _formatter = formatter;
    }

    public string Publish(string title, string content)
    {
        return _formatter.Format(title, content);
    }
}

Usage is straightforward. You pick a formatter, call Publish, and get the result:

var publisher = new DocumentPublisher(
    new MarkdownFormatter());

string result = publisher.Publish(
    "My Article", "Some content here.");
// result: "# My Article

Some content here."

publisher.SetFormatter(new HtmlFormatter());
result = publisher.Publish(
    "My Article", "Some content here.");
// result: "<h1>My Article</h1>
<p>Some content here.</p>"

The strategy doesn't know about the other formatters. The HtmlFormatter has no idea that MarkdownFormatter exists. The client code -- not the strategy -- decides which formatter to use. Compare this to the state pattern approach above where state objects actively transition the context between states. Strategies passively wait to be selected. This is the state vs strategy distinction at its clearest.

Combining State and Strategy Patterns

The state vs strategy pattern comparison doesn't have to be either-or. These patterns combine naturally when an object's behavior changes with state and each state needs configurable algorithms.

Consider a document system where the workflow stage (state) determines what kinds of operations are available, and the export format (strategy) determines how those operations execute:

public interface IExportStrategy
{
    string Export(string content);
}

public sealed class PdfExportStrategy : IExportStrategy
{
    public string Export(string content)
    {
        return $"[PDF Export] {content}";
    }
}

public sealed class WebExportStrategy : IExportStrategy
{
    public string Export(string content)
    {
        return $"<article>{content}</article>";
    }
}

public sealed class PublishedWithExportState : IDocumentState
{
    private readonly IExportStrategy _exportStrategy;

    public PublishedWithExportState(
        IExportStrategy exportStrategy)
    {
        _exportStrategy = exportStrategy;
    }

    public void Edit(DocumentContext context)
    {
        Console.WriteLine(
            "Cannot edit a published document.");
    }

    public void Submit(DocumentContext context)
    {
        string exported = _exportStrategy
            .Export(context.Content);
        Console.WriteLine(exported);
    }

    public void Approve(DocumentContext context)
    {
        Console.WriteLine("Already published.");
    }

    public void Reject(DocumentContext context)
    {
        context.SetState(new DraftState());
    }

    public string GetStatusDisplay() => "Published";
}

In this combined approach, the state pattern manages which lifecycle phase the document is in, while the strategy pattern determines how the export is performed. Each pattern handles a different axis of variability, and they compose cleanly.

This combination works well with inversion of control -- you can resolve different export strategies from a DI container and inject them into state objects as needed.

Decision Criteria: State vs Strategy

When deciding between the state vs strategy pattern, run through these questions in order:

Does the object's behavior change as it moves through lifecycle phases? If yes, use the state pattern. When the same method call should produce different behavior depending on what has happened before, you're dealing with state transitions.

Are you selecting between interchangeable algorithms for a single operation? If yes, use the strategy pattern. When the decision is about how to perform something, the strategy pattern fits naturally.

Should transitions happen automatically as part of normal operation? If yes, use the state pattern. If external code is managing transitions, you might be using strategies disguised as states.

Are the implementations independent of each other? If yes, use the strategy pattern. If your implementations need to know about and transition to other implementations, that's the state pattern.

Do you need both lifecycle behavior and algorithm flexibility? Combine both patterns.

Common State vs Strategy Mistakes

Developers make several recurring mistakes when choosing between the state vs strategy pattern or implementing either one.

Using strategies when you need state transitions. If you find yourself adding a SetStrategy call after every operation to simulate state changes, you've outgrown the strategy pattern. The state pattern encapsulates these transitions inside the state objects where they belong. Manually managing transitions outside the states defeats the purpose and creates fragile code.

Building states that don't transition. If your "state" objects never change the context's state, they're really just strategies with a confusing name. States must drive transitions -- that's what distinguishes them from strategies. If every method in your state implementation ignores the context parameter, reconsider whether you're actually modeling state.

Leaking transition logic into the context. In a proper state pattern implementation, the context delegates to the current state. If the context contains if/else or switch blocks to determine which state to enter, you've lost the primary benefit. Move transition logic into the state objects.

Creating strategies that know about each other. Strategies should be independent. If StrategyA references StrategyB or contains logic assuming a specific strategy was used previously, you've introduced coupling that the pattern is meant to eliminate.

Ignoring the observer pattern for state change notifications. When the context's state changes, other parts of the system often need to react -- updating UI, logging transitions, or triggering side effects. Combining the state pattern with an observer lets you decouple state transitions from their downstream effects.

Confusing state pattern with enum-based state machines. An enum with a switch statement is not the state pattern. The state pattern replaces conditional logic with polymorphism, where each state is a separate class with its own behavior.

Comparison Summary Table

Here's a quick-reference table summarizing the state vs strategy pattern differences:

Feature State Strategy
Intent Change behavior based on internal state Select an interchangeable algorithm
Who controls transitions State objects transition the context Client code selects the strategy
Awareness States know about other states Strategies are independent
Lifecycle State changes repeatedly over time Strategy is typically set once
Statefulness Context maintains current state Strategies are usually stateless
Transition trigger Internal -- driven by domain logic External -- driven by client code
Number of behaviors affected All methods change with state One algorithm is swapped
C# implementation Context delegates to current state object Context delegates to injected strategy
Typical use cases Workflows, protocols, UI modes Sorting, formatting, validation

Both patterns use composition and delegation. The structural similarity is what makes the state vs strategy comparison confusing, but the behavioral differences are substantial.

Frequently Asked Questions

What is the key difference between state and strategy pattern?

The key difference is who controls the behavior change and why. In the state pattern, behavior changes automatically as internal state evolves -- state objects drive transitions based on domain logic. In the strategy pattern, external code selects which algorithm to use, and the strategy never triggers a change. The state vs strategy distinction boils down to internal transitions vs external selection.

Can state objects be used as strategies?

Structurally, yes -- they implement the same delegation pattern. But the intent and behavior differ. A state object knows about other states, drives transitions, and represents a lifecycle phase. A strategy is a self-contained algorithm with no awareness of alternatives. Keep them separate to preserve each pattern's clarity.

When should I refactor from strategy to state pattern?

Refactor when your strategy-based code develops symptoms of hidden state management. If you're calling SetStrategy after every operation, if the order of strategy invocations matters, or if you're adding tracking variables to remember which strategy was used previously -- these are signs that your domain has lifecycle behavior that the state pattern handles naturally. The decorator pattern can sometimes mask this problem by wrapping strategies with state-tracking logic, but if the core issue is lifecycle transitions, switching to the state pattern is the cleaner solution.

Does the state pattern violate the open/closed principle?

It can if implemented carelessly. When states directly instantiate other states (like new PendingReviewState() inside DraftState), adding a new state requires modifying existing state classes. You can mitigate this by having states accept transition targets through constructor injection or by using a state factory. This brings inversion of control into the state pattern, making it more extensible.

Is the state pattern the same as a finite state machine?

The state pattern is one way to implement a finite state machine using object-oriented principles. A finite state machine is the concept -- defined states, transitions, and rules. The state pattern replaces conditional state-checking logic with polymorphic state objects, giving you better encapsulation and extensibility than enums with switch statements.

Should I use the state pattern for simple two-state toggling?

Probably not. If your object toggles between two states -- like active/inactive -- a simple boolean flag is clearer. Reach for the state pattern when you have three or more states with distinct behaviors and non-trivial transition rules.

Can state vs strategy both work with dependency injection in C#?

Yes. Strategies are a natural fit for DI because the client selects which implementation to inject using IServiceCollection. States work with DI too, but differently -- you typically inject a state factory or service provider to create state objects, since the context swaps states dynamically at runtime. Strategies are resolved once at composition time, while states are created throughout the object's lifetime.

Wrapping Up State vs Strategy Pattern in C#

The state vs strategy pattern in C# distinction is structural similarity hiding behavioral divergence. The state pattern changes behavior automatically as internal state evolves, with state objects driving their own transitions. The strategy pattern lets external code select an algorithm, and strategies never change themselves.

When your object's behavior changes as it progresses through lifecycle phases -- use the state pattern. When you're picking between interchangeable algorithms for a single operation -- use the strategy pattern. And when your system needs both, combine them. The difference is in how transitions happen, who controls them, and whether the implementations know about each other. Get those three questions right, and you'll pick the correct pattern every time.

Command vs Strategy Pattern in C#: Key Differences Explained

Compare command vs strategy pattern in C# with side-by-side code examples, key behavioral differences, and guidance on when to use each.

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.

State Design Pattern in C#: Complete Guide with Examples

Master the state design pattern in C# with practical examples showing state transitions, behavior changes, and finite state machine implementations.

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