Wednesday, July 29, 2026

State Design Pattern

Mastering Design Patterns in C# and ASP.NET Core

Part 4.8 – State Design Pattern

Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Behavioral Design Pattern
Difficulty: ⭐⭐⭐☆☆ Intermediate
Prerequisites: C#, OOP, Interfaces, SOLID Principles, Dependency Injection, ASP.NET Core


Table of Contents

  1. Introduction

  2. What is the State Design Pattern?

  3. Why Do We Need the State Pattern?

  4. The Problem with Large if-else and switch Statements

  5. Real-World Analogy

  6. State Pattern Terminology

  7. How the State Pattern Works

  8. UML Class Diagram

  9. Components of the State Pattern

  10. Complete C# Console Application

  11. Order Processing Example

  12. State Transitions

  13. ASP.NET Core Implementation

  14. Banking Account Example

  15. E-Commerce Example

  16. State Pattern and State Machines

  17. State vs Strategy Pattern

  18. State vs Command Pattern

  19. State vs Chain of Responsibility

  20. Advantages

  21. Disadvantages

  22. Best Practices

  23. Common Mistakes

  24. Real-World Enterprise Scenarios

  25. Interview Questions

  26. Key Takeaways

  27. Conclusion

  28. Coming Up Next


1. Introduction

In real-world applications, an object's behavior often changes depending on its current state.

Consider an e-commerce order.

An order can move through several states:

Pending
   ↓
Confirmed
   ↓
Processing
   ↓
Shipped
   ↓
Delivered

But what happens when the order is cancelled?

Pending → Cancelled
Confirmed → Cancelled
Processing → Cancelled

And perhaps:

Delivered → Cannot Cancel

A common implementation is a large if-else or switch statement:

if (order.Status == "Pending")
{
    // ...
}
else if (order.Status == "Confirmed")
{
    // ...
}
else if (order.Status == "Shipped")
{
    // ...
}

As the number of states increases, this code becomes difficult to maintain.

The State Design Pattern provides a cleaner approach by moving state-specific behavior into separate classes.


2. What is the State Design Pattern?

The State Design Pattern allows an object to change its behavior when its internal state changes.

From the outside, it can appear as though the object itself has changed its class.

The core idea is:

Context
   |
   ↓
Current State
   |
   +---- State-specific behavior

Instead of writing:

switch (order.Status)
{
    case "Pending":
        ...
        break;

    case "Confirmed":
        ...
        break;

    case "Shipped":
        ...
        break;
}

we create separate classes:

PendingState
ConfirmedState
ProcessingState
ShippedState
DeliveredState
CancelledState

Each state knows what behavior is valid for that state.


3. Why Do We Need the State Pattern?

Suppose we have:

public class Order
{
    public string Status { get; set; }

    public void Cancel()
    {
        if (Status == "Pending")
        {
            // Cancel
        }
        else if (Status == "Confirmed")
        {
            // Cancel
        }
        else if (Status == "Processing")
        {
            // Cancel
        }
        else if (Status == "Shipped")
        {
            // Cannot cancel
        }
        else if (Status == "Delivered")
        {
            // Cannot cancel
        }
    }
}

This is manageable with a few states.

But enterprise applications can have:

10+ states
20+ operations
Multiple business rules
Different permissions
Different transitions

The number of conditions can grow rapidly.

The State Pattern separates these behaviors.


4. The Problem with Large if-else and switch Statements

Imagine an order has:

Pending
Confirmed
Paid
Processing
Packed
Shipped
OutForDelivery
Delivered
Cancelled
Returned
Refunded

Now imagine operations such as:

Confirm()
Pay()
Cancel()
Ship()
Deliver()
Return()
Refund()

The code can become:

Order
 |
 +-- switch(Status)
      |
      +-- Confirm
      +-- Pay
      +-- Cancel
      +-- Ship
      +-- Deliver
      +-- Return
      +-- Refund

This leads to:

  • Large classes

  • Difficult testing

  • Repeated conditions

  • Difficult maintenance

  • High risk when adding states

  • Violations of the Open/Closed Principle

The State Pattern replaces this with:

Order
 |
 +-- PendingState
 +-- ConfirmedState
 +-- PaidState
 +-- ProcessingState
 +-- ShippedState
 +-- DeliveredState
 +-- CancelledState

5. Real-World Analogy

Consider a traffic signal.

The traffic signal has states:

RED
YELLOW
GREEN

Its behavior depends on its current state.

Red

Stop

Green

Go

Yellow

Prepare to stop

We can model this as:

TrafficLight
     |
     +---- RedState
     |
     +---- YellowState
     |
     +---- GreenState

Each state controls the behavior associated with that state.


6. State Pattern Terminology

The pattern contains three primary concepts.

Context

The object whose behavior changes.

Example:

Order

State

An interface or abstraction defining state-specific behavior.

public interface IOrderState
{
    void Handle(Order order);
}

Concrete State

Specific implementations of the State interface.

PendingState
ConfirmedState
ShippedState
DeliveredState

The structure is:

             +----------------+
             |    Context     |
             +----------------+
             | currentState   |
             +-------+--------+
                     |
                     ↓
             +---------------+
             |   IState      |
             +---------------+
             | Handle()      |
             +-------+-------+
                     |
          +----------+----------+
          |          |          |
          ↓          ↓          ↓
       State A    State B    State C

7. How the State Pattern Works

Consider an order.

Initially:

Order
 ↓
PendingState

When confirmed:

Order
 ↓
ConfirmedState

When shipped:

Order
 ↓
ShippedState

When delivered:

Order
 ↓
DeliveredState

The object's behavior changes because the current State object changes.

+----------------+
| Order Context  |
+----------------+
       |
       ↓
PendingState
       |
       ↓
ConfirmedState
       |
       ↓
ShippedState
       |
       ↓
DeliveredState

8. UML Class Diagram

A typical State Pattern UML diagram:

                 +---------------------+
                 |       Context       |
                 +---------------------+
                 | - state: IState     |
                 +---------------------+
                 | + SetState()        |
                 | + Request()         |
                 +----------+----------+
                            |
                            ↓
                 +---------------------+
                 |       IState        |
                 +---------------------+
                 | + Handle()          |
                 +----------+----------+
                            |
              +-------------+-------------+
              |             |             |
              ↓             ↓             ↓
       +-------------+ +-------------+ +-------------+
       | State A     | | State B     | | State C     |
       +-------------+ +-------------+ +-------------+
       | Handle()    | | Handle()    | | Handle()    |
       +-------------+ +-------------+ +-------------+

9. Components of the State Pattern

9.1 Context

The Context maintains the current state.

public class Order
{
    private IOrderState _state;

    public Order(IOrderState state)
    {
        _state = state;
    }

    public void SetState(IOrderState state)
    {
        _state = state;
    }

    public void Process()
    {
        _state.Handle(this);
    }
}

9.2 State Interface

public interface IOrderState
{
    void Handle(Order order);
}

9.3 Concrete States

public class PendingState : IOrderState
{
    public void Handle(Order order)
    {
        Console.WriteLine(
            "Order is pending.");
    }
}

Another:

public class ShippedState : IOrderState
{
    public void Handle(Order order)
    {
        Console.WriteLine(
            "Order has been shipped.");
    }
}

10. Complete C# Console Application

Let's build a complete order-processing example.

Step 1 – State Interface

public interface IOrderState
{
    void Confirm(Order order);

    void Ship(Order order);

    void Deliver(Order order);

    void Cancel(Order order);
}

The state interface defines operations that can behave differently based on the current state.


11. Context – Order

public class Order
{
    private IOrderState _state;

    public Order()
    {
        _state = new PendingState();
    }

    public void SetState(IOrderState state)
    {
        _state = state;
    }

    public void Confirm()
    {
        _state.Confirm(this);
    }

    public void Ship()
    {
        _state.Ship(this);
    }

    public void Deliver()
    {
        _state.Deliver(this);
    }

    public void Cancel()
    {
        _state.Cancel(this);
    }
}

Notice that Order does not contain:

if status == Pending
if status == Confirmed
if status == Shipped

The behavior is delegated to the current state.


12. Pending State

public class PendingState : IOrderState
{
    public void Confirm(Order order)
    {
        Console.WriteLine(
            "Order confirmed.");

        order.SetState(
            new ConfirmedState());
    }

    public void Ship(Order order)
    {
        Console.WriteLine(
            "Cannot ship a pending order.");
    }

    public void Deliver(Order order)
    {
        Console.WriteLine(
            "Cannot deliver a pending order.");
    }

    public void Cancel(Order order)
    {
        Console.WriteLine(
            "Order cancelled.");

        order.SetState(
            new CancelledState());
    }
}

13. Confirmed State

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

    public void Ship(Order order)
    {
        Console.WriteLine(
            "Order shipped.");

        order.SetState(
            new ShippedState());
    }

    public void Deliver(Order order)
    {
        Console.WriteLine(
            "Cannot deliver before shipping.");
    }

    public void Cancel(Order order)
    {
        Console.WriteLine(
            "Order cancelled.");

        order.SetState(
            new CancelledState());
    }
}

14. Shipped State

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

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

    public void Deliver(Order order)
    {
        Console.WriteLine(
            "Order delivered.");

        order.SetState(
            new DeliveredState());
    }

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

15. Delivered State

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

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

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

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

16. Cancelled State

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

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

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

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

17. Program

var order = new Order();

order.Confirm();

order.Ship();

order.Deliver();

order.Cancel();

Output:

Order confirmed.
Order shipped.
Order delivered.
Cannot cancel a delivered order.

The important point is that the same:

order.Cancel();

method behaves differently depending on the current state.


18. Understanding the State Transition

The order starts here:

PendingState

After:

order.Confirm();

it becomes:

ConfirmedState

Then:

order.Ship();

changes it to:

ShippedState

Then:

order.Deliver();

changes it to:

DeliveredState

Therefore:

Pending
   |
   | Confirm()
   ↓
Confirmed
   |
   | Ship()
   ↓
Shipped
   |
   | Deliver()
   ↓
Delivered

19. Order Cancellation Flow

Another possible transition:

Pending
   |
   | Cancel()
   ↓
Cancelled

Or:

Confirmed
   |
   | Cancel()
   ↓
Cancelled

But:

Shipped
   |
   | Cancel()
   ↓
Not Allowed

This is where the State Pattern becomes very useful.


20. State Transition Diagram

For a larger order workflow:

                         +-------------+
                         |   Pending   |
                         +------+------+
                                |
                             Confirm
                                |
                                ↓
                         +-------------+
                         |  Confirmed  |
                         +------+------+
                                |
                              Ship
                                |
                                ↓
                         +-------------+
                         |   Shipped   |
                         +------+------+
                                |
                             Deliver
                                |
                                ↓
                         +-------------+
                         |  Delivered  |
                         +-------------+

Pending ----------------------> Cancelled
Confirmed --------------------> Cancelled

This is effectively a simple state machine.


21. ASP.NET Core Implementation

Let's build a more realistic ASP.NET Core example.

Suppose we have:

Order API

and the order has states:

Pending
Confirmed
Shipped
Delivered
Cancelled

We can use dependency injection to manage the states.


22. State Interface

public interface IOrderState
{
    string Name { get; }

    Task ConfirmAsync(OrderContext context);

    Task ShipAsync(OrderContext context);

    Task DeliverAsync(OrderContext context);

    Task CancelAsync(OrderContext context);
}

23. Order Context

public class OrderContext
{
    private IOrderState _state;

    public int OrderId { get; }

    public OrderContext(
        int orderId,
        IOrderState initialState)
    {
        OrderId = orderId;
        _state = initialState;
    }

    public string StateName =>
        _state.Name;

    public void SetState(
        IOrderState state)
    {
        _state = state;
    }

    public Task ConfirmAsync()
    {
        return _state.ConfirmAsync(this);
    }

    public Task ShipAsync()
    {
        return _state.ShipAsync(this);
    }

    public Task DeliverAsync()
    {
        return _state.DeliverAsync(this);
    }

    public Task CancelAsync()
    {
        return _state.CancelAsync(this);
    }
}

24. Pending State

public class PendingOrderState
    : IOrderState
{
    public string Name => "Pending";

    public Task ConfirmAsync(
        OrderContext context)
    {
        Console.WriteLine(
            $"Order {context.OrderId} confirmed.");

        return Task.CompletedTask;
    }

    public Task ShipAsync(
        OrderContext context)
    {
        throw new InvalidOperationException(
            "Pending order cannot be shipped.");
    }

    public Task DeliverAsync(
        OrderContext context)
    {
        throw new InvalidOperationException(
            "Pending order cannot be delivered.");
    }

    public Task CancelAsync(
        OrderContext context)
    {
        Console.WriteLine(
            $"Order {context.OrderId} cancelled.");

        return Task.CompletedTask;
    }
}

For a production application, the state transition would generally also update persistent order state in a database.


25. Dependency Injection

Register the states:

builder.Services.AddTransient<
    PendingOrderState>();

builder.Services.AddTransient<
    ConfirmedOrderState>();

builder.Services.AddTransient<
    ShippedOrderState>();

builder.Services.AddTransient<
    DeliveredOrderState>();

builder.Services.AddTransient<
    CancelledOrderState>();

You can also register them against a common abstraction:

builder.Services.AddTransient<
    IOrderState,
    PendingOrderState>();

builder.Services.AddTransient<
    IOrderState,
    ConfirmedOrderState>();

builder.Services.AddTransient<
    IOrderState,
    ShippedOrderState>();

builder.Services.AddTransient<
    IOrderState,
    DeliveredOrderState>();

builder.Services.AddTransient<
    IOrderState,
    CancelledOrderState>();

Then ASP.NET Core can resolve all implementations through:

IEnumerable<IOrderState>

26. Controller Example

[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly IEnumerable<IOrderState>
        _states;

    public OrdersController(
        IEnumerable<IOrderState> states)
    {
        _states = states;
    }

    [HttpPost("{id}/confirm")]
    public IActionResult Confirm(int id)
    {
        var state = _states
            .First(x => x.Name == "Pending");

        var order = new OrderContext(
            id,
            state);

        order.ConfirmAsync();

        return Ok(new
        {
            OrderId = id,
            State = "Confirmed"
        });
    }
}

In a real application, you would normally load the current state from the database rather than constructing it directly inside the controller.


27. Better Enterprise Architecture

For an enterprise application, avoid putting state-management logic directly inside the controller.

A better structure is:

Controller
    ↓
Application Service
    ↓
Order State Manager
    ↓
State Object
    ↓
Repository
    ↓
Database

For example:

POST /api/orders/100/ship
            |
            ↓
      OrdersController
            |
            ↓
      OrderService
            |
            ↓
     Current State
            |
            ↓
    ShippedState
            |
            ↓
    Update Database

This keeps responsibilities separated.


28. Database State vs State Pattern

This is an important enterprise consideration.

The database might contain:

OrderId = 1001
Status = "Shipped"

The State Pattern then maps the persisted status to behavior:

"Pending"   → PendingState
"Confirmed" → ConfirmedState
"Shipped"   → ShippedState
"Delivered" → DeliveredState

So:

Database
   ↓
Current Status
   ↓
State Object
   ↓
Behavior

This approach is useful when state-specific business behavior is complex.


29. Banking Account Example

A bank account can also have states.

For example:

Active
Suspended
Blocked
Closed

Operations:

Deposit
Withdraw
Transfer
Close

Behavior can depend on the state.

Active

Deposit → Allowed
Withdraw → Allowed
Transfer → Allowed

Suspended

Deposit → Allowed
Withdraw → Restricted
Transfer → Restricted

Closed

Deposit → Not Allowed
Withdraw → Not Allowed
Transfer → Not Allowed

Instead of:

if (status == "Active")
{
    ...
}
else if (status == "Suspended")
{
    ...
}
else if (status == "Closed")
{
    ...
}

each state can define the appropriate behavior.


30. E-Commerce Example

An order workflow might look like:

Pending
   ↓
PaymentProcessing
   ↓
Paid
   ↓
Packing
   ↓
Shipped
   ↓
Delivered

Additional states:

Cancelled
PaymentFailed
Returned
Refunded

State-specific operations can include:

Cancel
Ship
Return
Refund
RetryPayment

This can quickly become complicated using only conditionals.

The State Pattern allows the behavior to be distributed across state classes.


31. State Pattern and State Machines

The State Pattern is closely related to Finite State Machines (FSMs).

An FSM consists of:

States
Events
Transitions
Actions

For example:

                Confirm
Pending ------------------> Confirmed
   |                           |
   | Cancel                    | Ship
   ↓                           ↓
Cancelled                    Shipped
                                |
                                | Deliver
                                ↓
                            Delivered

This can be represented as:

Current State
      +
Event
      ↓
Transition
      ↓
New State

32. State Pattern vs State Machine

They are related but not identical.

State Pattern

Primarily focuses on:

Object behavior based on current state

State Machine

Primarily focuses on:

Valid states
Events
Transitions
Transition rules

A complex enterprise workflow may use a state-machine library or a dedicated workflow engine instead of implementing every transition manually.


33. State vs Strategy Pattern

This is one of the most common interview questions.

They look similar because both use composition and interfaces.

Strategy

The client chooses an algorithm.

OrderService
     |
     +---- CreditCardStrategy
     +---- PayPalStrategy
     +---- BankTransferStrategy

Example:

Choose payment algorithm

State

The object's behavior changes because its state changes.

Order
 ↓
PendingState
 ↓
ConfirmedState
 ↓
ShippedState

Key Difference

Strategy:

Which algorithm should I use?

State:

What behavior is appropriate for my current state?


34. State vs Command

Command

Encapsulates a request or operation.

ShipOrderCommand
CancelOrderCommand
RefundOrderCommand

State

Controls behavior based on the object's current state.

PendingState
ShippedState
DeliveredState

For example:

Command = CancelOrder
State = Shipped

The Shipped State can determine:

Cancel → Not Allowed

So the two patterns can work together.


35. State vs Chain of Responsibility

State

The object has one current state:

Order
 ↓
Current State

Chain of Responsibility

A request moves through a sequence of handlers:

Request
 ↓
Handler A
 ↓
Handler B
 ↓
Handler C

State determines behavior based on current condition.

Chain of Responsibility determines which handler should process a request.


36. Advantages of State Pattern

1. Eliminates Large Conditional Statements

Instead of:

if
else if
else if
else if

we use:

State Classes

2. Single Responsibility

Each state handles its own behavior.


3. Easier Maintenance

Changes to ShippedState don't necessarily affect PendingState.


4. Better Extensibility

Adding:

ReturnedState

can be done independently.


5. Clear State Transitions

Transitions can be expressed explicitly:

Pending → Confirmed
Confirmed → Shipped
Shipped → Delivered

6. Better Testability

Each state can be unit tested independently.


37. Disadvantages

1. More Classes

A simple if statement may become:

PendingState.cs
ConfirmedState.cs
ShippedState.cs
DeliveredState.cs
CancelledState.cs

This can be unnecessary for simple workflows.


2. State Transition Complexity

With many states, transitions themselves can become difficult to manage.


3. Increased Abstraction

Developers need to understand:

Context
State
Concrete States
Transitions

4. Persistence Complexity

If state is stored in a database, the application needs a reliable mapping between:

Database State

and:

State Object

5. Overengineering Risk

Don't use State Pattern simply because an application has two or three statuses.

Use it when state-specific behavior is sufficiently complex to justify the abstraction.


38. Best Practices

1. Use Strongly Typed State Representation

Avoid scattering strings such as:

"Pending"
"Shipped"
"Delivered"

throughout the code.

Prefer centralized state definitions.


2. Keep State Classes Focused

Each state should contain behavior relevant to that state.


3. Make Invalid Transitions Explicit

For example:

Delivered → Ship

should clearly be rejected.


4. Keep Persistence Separate

Don't make state classes responsible for every database operation.

Prefer:

State
 ↓
Application Service
 ↓
Repository

where appropriate.


5. Unit Test State Transitions

Test:

Pending → Confirmed
Confirmed → Shipped
Shipped → Delivered

and invalid transitions.


6. Document the State Diagram

For complex workflows, a state-transition diagram can be extremely valuable.


7. Consider a State Machine for Complex Workflows

If there are dozens of states and transitions, a dedicated state-machine approach may be easier to maintain than hand-written state classes.


39. Common Mistakes

Mistake 1 – Using State Pattern for Every Enum

Not every enum requires a State Pattern.


Mistake 2 – Mixing State and Persistence

Avoid making every state class directly responsible for database access.


Mistake 3 – Allowing Invalid Transitions

Make transitions explicit.


Mistake 4 – Creating Huge State Classes

If one state class becomes enormous, reconsider your responsibilities.


Mistake 5 – Circular State Dependencies

Be careful when state objects directly create each other:

PendingState
   ↓
ConfirmedState
   ↓
ShippedState

For complex applications, a state manager or factory can centralize state creation.


Mistake 6 – Ignoring Concurrency

In enterprise applications, two requests could attempt:

Ship Order
Cancel Order

at almost the same time.

State validation alone does not solve database concurrency.

Use appropriate transactional and concurrency mechanisms.


40. Real-World Enterprise Scenarios

The State Pattern can be useful in:

E-Commerce

Pending
Confirmed
Paid
Packed
Shipped
Delivered
Returned
Refunded

Banking

Active
Suspended
Blocked
Closed

Payment Processing

Created
Processing
Authorized
Captured
Failed
Refunded

Insurance Claims

Submitted
UnderReview
Approved
Rejected
Settled
Closed

Loan Processing

ApplicationSubmitted
UnderReview
Approved
Rejected
Disbursed
Closed

Ticketing Systems

Open
Assigned
InProgress
Resolved
Closed
Reopened

Document Approval

Draft
Submitted
UnderReview
Approved
Rejected
Published

41. State Pattern with SOLID Principles

The State Pattern naturally supports several SOLID principles.

Single Responsibility Principle

Each state class focuses on state-specific behavior.


Open/Closed Principle

New states can often be added without modifying every existing state.


Dependency Inversion Principle

The Context works with:

IOrderState

rather than concrete implementations.


42. State Pattern Testing

Suppose we have:

Pending
Confirmed
Shipped
Delivered

We should test valid transitions.

Test 1

Pending → Confirmed

Test 2

Confirmed → Shipped

Test 3

Shipped → Delivered

Test 4

Delivered → Cancelled

Expected:

Rejected

Test 5

Pending → Delivered

Expected:

Rejected

State-based testing becomes much easier when each state's behavior is isolated.


43. Interview Questions

Beginner

1. What is the State Design Pattern?

It allows an object to change its behavior when its internal state changes.


2. What problem does the State Pattern solve?

It helps eliminate complex conditional logic where behavior depends heavily on an object's current state.


3. What are the main components?

Context
State
Concrete State

4. What is the Context?

The object whose behavior changes depending on its current state.


5. What is a Concrete State?

A class implementing behavior for a particular state.


Intermediate

6. How does State reduce if-else statements?

Instead of:

if (status == "Pending")
{
}
else if (status == "Shipped")
{
}

we use:

PendingState
ShippedState

Each class contains the appropriate behavior.


7. State Pattern vs Strategy Pattern?

Strategy chooses an algorithm.

State changes behavior based on the object's current state.


8. State Pattern vs Command?

Command encapsulates a request.

State determines how the object behaves when that request is received.


9. State Pattern vs Chain of Responsibility?

State represents behavior based on current state.

Chain of Responsibility passes a request through handlers.


10. Can State Pattern work with Dependency Injection?

Yes.

ASP.NET Core can register state implementations and inject them through abstractions such as:

IEnumerable<IOrderState>

44. Advanced Interview Questions

11. When should you use State Pattern instead of an enum?

Use the State Pattern when states have significantly different behavior or transition rules.

If the enum is only used for display or simple comparisons, State Pattern may be unnecessary.


12. Can State Pattern be used with databases?

Yes.

The database stores the current state, and the application maps that state to the appropriate behavior object.


13. Is State Pattern suitable for microservices?

It can be used within an individual service.

For distributed workflows, however, additional tools may be appropriate:

  • State machines

  • Workflow engines

  • Durable messaging

  • Saga orchestration

  • Event-driven architecture


14. How do you prevent invalid state transitions?

Centralize transition rules and explicitly reject invalid operations.

For example:

Delivered
   |
   +-- Cancel → Not Allowed
   +-- Ship   → Not Allowed

15. How would you persist state?

A common approach is:

Order
----------------
Id
Status
CreatedDate
UpdatedDate

The Status is persisted in the database.

At runtime:

Status
 ↓
State Factory
 ↓
Concrete State

16. What is the difference between State Pattern and State Machine?

State Pattern focuses on object behavior based on state.

A state machine focuses more explicitly on states, events, transitions, and transition rules.


17. Can State Pattern improve testability?

Yes.

Each concrete state can be tested independently.


18. Is State Pattern always better than switch?

No.

For a small number of simple states, a switch can be clearer.

The State Pattern becomes valuable when state-specific behavior becomes complex and frequently changes.


19. Can State objects be stateless?

Yes.

If a State object contains no instance-specific data, it can potentially be reused depending on the application's design.


20. What is a major warning sign that State Pattern is needed?

A strong indication is a class containing many repeated conditions such as:

if (status == ...)

across many methods, where each status causes substantially different behavior.


45. Practical Architecture Example

A production e-commerce system could look like:

                    API Request
                        |
                        ↓
                OrdersController
                        |
                        ↓
                  OrderService
                        |
                        ↓
                Current Order State
                        |
         +--------------+--------------+
         |              |              |
         ↓              ↓              ↓
    PendingState   ShippedState   DeliveredState
         |              |              |
         +--------------+--------------+
                        |
                        ↓
                  Order Repository
                        |
                        ↓
                     Database

For example:

POST /api/orders/100/ship

The application:

1. Loads Order 100
2. Reads current status
3. Creates/resolves corresponding State
4. Executes Ship()
5. Validates transition
6. Changes state
7. Persists new status
8. Publishes an event if necessary

46. State Pattern + Observer Pattern

The State Pattern can also work together with the Observer Pattern.

For example:

Order
 ↓
State Changes
 ↓
ShippedState
 ↓
OrderStatusChanged Event
 ↓
Observers
 ├── Email
 ├── SMS
 ├── Audit
 └── Analytics

Here:

State Pattern handles:

What behavior is valid for the current state?

Observer Pattern handles:

Who needs to know that the state changed?

This combination is extremely useful in enterprise applications.


47. State Pattern + Command Pattern

These patterns can also complement one another.

For example:

CancelOrderCommand
        |
        ↓
      Order
        |
        ↓
   Current State
        |
        ↓
Can Cancel?

If the order is:

PendingState

then:

Cancel → Allowed

If it is:

ShippedState

then:

Cancel → Rejected

So:

Command = What operation is requested?

State = Is that operation valid, and how should it behave?

48. State Pattern + CQRS

The State Pattern can also be useful in applications implementing CQRS.

For example:

ShipOrderCommand
       |
       ↓
ShipOrderHandler
       |
       ↓
Order
       |
       ↓
Current State
       |
       ↓
ShippedState

The State Pattern handles state-specific domain behavior, while CQRS separates commands and queries.

These patterns solve different problems and can work together.


49. When Should You Use the State Pattern?

Use State Pattern when:

  • An object has many distinct states.

  • Behavior changes significantly between states.

  • State-specific rules are becoming complex.

  • Large if-else or switch statements are growing.

  • State transitions are important business rules.

  • You need to test each state independently.

  • New states are expected to be introduced over time.


50. When Should You NOT Use It?

Avoid State Pattern when:

  • There are only one or two simple states.

  • State-specific behavior is trivial.

  • A simple switch is much clearer.

  • The abstraction creates more classes than value.

  • There are no meaningful state transitions.

Remember:

Design patterns are tools, not mandatory rules.


51. Key Takeaways

The most important concepts to remember are:

1. State is a Behavioral Design Pattern

It focuses on changing behavior based on an object's state.

2. It reduces complex conditional logic

Instead of:

Large if/else

use:

State classes

3. The Context owns the current State

Context
   ↓
Current State

4. State transitions are important

Pending
   ↓
Confirmed
   ↓
Shipped
   ↓
Delivered

5. State and Strategy are different

Strategy selects an algorithm.

State represents behavior associated with the current state.

6. State and Command can work together

Command represents an operation.

State determines whether and how that operation should execute.

7. State and Observer can work together

State handles behavior.

Observer handles notification of state changes.

8. State machines are useful for complex workflows

For highly complex workflows, consider dedicated state-machine or workflow solutions.


Conclusion

The State Design Pattern is an excellent solution when an object's behavior changes significantly depending on its current state.

Instead of creating a massive class containing:

if (...)
else if (...)
else if (...)
else if (...)

we can model each state separately:

PendingState
ConfirmedState
ShippedState
DeliveredState
CancelledState

The architecture becomes:

                    Context
                       |
                       ↓
                  Current State
                       |
          +------------+------------+
          |            |            |
          ↓            ↓            ↓
       Pending      Shipped      Delivered

This approach improves:

  • Maintainability

  • Readability

  • Testability

  • Extensibility

  • Separation of responsibilities

In modern .NET applications, the State Pattern is particularly useful for:

  • Order workflows

  • Payment processing

  • Banking systems

  • Insurance claims

  • Approval workflows

  • Ticketing systems

  • Document workflows

  • Loan processing

  • Business process management

The key lesson is:

Use the State Pattern when an object's behavior changes substantially according to its current state and conditional logic is becoming difficult to maintain.


🚀 Coming Up Next: Part 4.9 – Strategy Design Pattern

In the next article, we'll explore the Strategy Design Pattern, including:

  • What is the Strategy Pattern?

  • Why do we need it?

  • Encapsulating algorithms

  • Replacing large if-else and switch statements

  • Strategy and Context concepts

  • UML Class Diagram

  • Complete C# Console Application

  • Payment processing example

  • Discount calculation example

  • ASP.NET Core implementation

  • Dependency Injection with Strategy

  • Strategy Factory

  • Strategy vs State

  • Strategy vs Command

  • Strategy vs Template Method

  • Strategy vs Chain of Responsibility

  • Real-world enterprise scenarios

  • Advantages and disadvantages

  • Best practices

  • Common mistakes

  • Interview questions

The Strategy Pattern is one of the most useful patterns for modern .NET applications because it allows algorithms and business rules to be changed independently without modifying the code that uses them.

Observer Design Pattern


Mastering Design Patterns in C# and ASP.NET Core

Part 4.7 – Observer Design Pattern

Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Behavioral Design Pattern
Difficulty: ⭐⭐⭐☆☆ Intermediate
Prerequisites: C#, OOP, Interfaces, Delegates, Events, Dependency Injection, ASP.NET Core


Table of Contents

  1. Introduction

  2. What is the Observer Design Pattern?

  3. Why Do We Need the Observer Pattern?

  4. Real-World Analogy

  5. Subject and Observer Concepts

  6. How the Observer Pattern Works

  7. UML Class Diagram

  8. Components of the Observer Pattern

  9. Complete C# Console Application

  10. Observer Pattern Using C# Events

  11. IObservable<T> and IObserver<T>

  12. ASP.NET Core Implementation

  13. Banking Notification Example

  14. E-Commerce Order Notification Example

  15. Stock Price Monitoring Example

  16. Event-Driven Architecture

  17. Observer vs Pub/Sub

  18. Observer vs Mediator

  19. Observer vs Event-Driven Architecture

  20. Advantages

  21. Disadvantages

  22. Best Practices

  23. Common Mistakes

  24. Real-World Enterprise Scenarios

  25. Interview Questions

  26. Key Takeaways

  27. Conclusion

  28. Coming Up Next


1. Introduction

In real-world applications, one object often needs to notify multiple other objects when something changes.

For example:

Order Status Changed
        |
        +----> Email Notification
        |
        +----> SMS Notification
        |
        +----> Push Notification
        |
        +----> Audit Logging
        |
        +----> Analytics

A common question is:

How can one object notify multiple dependent objects without becoming tightly coupled to them?

The answer is the Observer Design Pattern.

The Observer Pattern is one of the most useful behavioral patterns in modern software development and has strong connections with:

  • C# events

  • Delegates

  • IObservable<T>

  • Reactive programming

  • Notification systems

  • Event-driven applications

  • UI updates

  • Domain events


2. What is the Observer Design Pattern?

The Observer Design Pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependent objects are automatically notified.

The object being observed is generally called the:

Subject

The objects receiving notifications are called:

Observers

The basic structure is:

             Subject
                |
       State changes
                |
        +-------+-------+
        |       |       |
        ↓       ↓       ↓
    Observer Observer Observer

When the Subject changes:

Subject
   |
   | Notify()
   |
   +------> Observer 1
   |
   +------> Observer 2
   |
   +------> Observer 3

The Subject does not need to know the detailed implementation of each Observer.


3. Why Do We Need the Observer Pattern?

Consider an e-commerce application.

When an order status changes:

Order #1001
Status = Shipped

Several things may need to happen:

Order Status Changed
       |
       +---- Email Customer
       |
       +---- Send SMS
       |
       +---- Push Notification
       |
       +---- Update Analytics
       |
       +---- Write Audit Log

A poor implementation might look like:

public void UpdateOrderStatus()
{
    SendEmail();

    SendSms();

    SendPushNotification();

    UpdateAnalytics();

    WriteAuditLog();
}

This creates strong coupling.

The OrderService now knows about:

  • Email service

  • SMS service

  • Push notification service

  • Analytics service

  • Audit service

If tomorrow we add:

WhatsApp Notification

the order service must change.

The Observer Pattern allows us to separate these responsibilities.


4. Real-World Analogy

Imagine subscribing to a YouTube channel.

You are an observer.

The YouTube channel is the subject.

When a new video is published:

YouTube Channel
       |
       | New Video
       ↓
Subscribers
       |
       +---- Subscriber A
       +---- Subscriber B
       +---- Subscriber C
       +---- Subscriber D

The channel doesn't need to individually contact every subscriber using custom logic.

Subscribers register themselves.

When something happens, the subscribers receive a notification.

This is the basic idea behind the Observer Pattern.


5. Subject and Observer Concepts

The pattern usually consists of two main abstractions.

Subject

Responsible for:

  • Maintaining observers

  • Adding observers

  • Removing observers

  • Notifying observers

Example:

public interface ISubject
{
    void Attach(IObserver observer);

    void Detach(IObserver observer);

    void Notify();
}

Observer

Responsible for responding to notifications.

public interface IObserver
{
    void Update();
}

The Subject communicates with observers through the abstraction.


6. How the Observer Pattern Works

The lifecycle is:

Step 1 – Create Subject

StockPrice

Step 2 – Create Observers

Mobile App
Web App
Trading Dashboard

Step 3 – Subscribe

StockPrice
    |
    +---- Mobile App
    +---- Web App
    +---- Dashboard

Step 4 – State Changes

Stock Price
$150 → $155

Step 5 – Notify

Notify()

Step 6 – Observers React

Mobile App → Update
Web App → Update
Dashboard → Update

7. UML Class Diagram

A traditional Observer Pattern UML diagram looks like this:

                    +----------------------+
                    |       Subject        |
                    +----------------------+
                    | - observers          |
                    +----------------------+
                    | + Attach()           |
                    | + Detach()           |
                    | + Notify()            |
                    +----------+-----------+
                               |
                               |
                    +----------v-----------+
                    |     IObserver         |
                    +-----------------------+
                    | + Update()            |
                    +----------+------------+
                               |
                 +-------------+-------------+
                 |                           |
        +--------v---------+       +---------v--------+
        | ConcreteObserver |       | ConcreteObserver |
        +------------------+       +------------------+
        | + Update()       |       | + Update()       |
        +------------------+       +------------------+

The key relationship is:

Subject → many Observers

8. Components of the Observer Pattern

8.1 Subject

Maintains the collection of observers.

public interface ISubject
{
    void Attach(IObserver observer);

    void Detach(IObserver observer);

    void Notify();
}

8.2 Concrete Subject

Contains the actual state.

public class Stock : ISubject
{
    private readonly List<IObserver> _observers = new();

    public decimal Price { get; private set; }

    public void Attach(IObserver observer)
    {
        _observers.Add(observer);
    }

    public void Detach(IObserver observer)
    {
        _observers.Remove(observer);
    }

    public void Notify()
    {
        foreach (var observer in _observers)
        {
            observer.Update();
        }
    }

    public void SetPrice(decimal price)
    {
        Price = price;
        Notify();
    }
}

8.3 Observer

public interface IObserver
{
    void Update();
}

8.4 Concrete Observer

public class MobileApp : IObserver
{
    public void Update()
    {
        Console.WriteLine(
            "Mobile App received stock update.");
    }
}

9. Complete C# Console Application

Let's build a complete stock price notification application.

Step 1 – Observer Interface

public interface IObserver
{
    void Update(decimal price);
}

Step 2 – Subject Interface

public interface ISubject
{
    void Attach(IObserver observer);

    void Detach(IObserver observer);

    void Notify();
}

Step 3 – Concrete Subject

public class Stock : ISubject
{
    private readonly List<IObserver> _observers = new();

    public string Symbol { get; }

    public decimal Price { get; private set; }

    public Stock(string symbol)
    {
        Symbol = symbol;
    }

    public void Attach(IObserver observer)
    {
        _observers.Add(observer);
    }

    public void Detach(IObserver observer)
    {
        _observers.Remove(observer);
    }

    public void Notify()
    {
        foreach (var observer in _observers)
        {
            observer.Update(Price);
        }
    }

    public void SetPrice(decimal price)
    {
        Price = price;

        Console.WriteLine(
            $"{Symbol} price changed to {Price:C}");

        Notify();
    }
}

Step 4 – Concrete Observers

Mobile Application

public class MobileApp : IObserver
{
    public void Update(decimal price)
    {
        Console.WriteLine(
            $"Mobile App: Stock price updated to {price:C}");
    }
}

Web Dashboard

public class WebDashboard : IObserver
{
    public void Update(decimal price)
    {
        Console.WriteLine(
            $"Web Dashboard: Stock price updated to {price:C}");
    }
}

Trading System

public class TradingSystem : IObserver
{
    public void Update(decimal price)
    {
        Console.WriteLine(
            $"Trading System: Received price {price:C}");
    }
}

Step 5 – Program

var stock = new Stock("MSFT");

var mobileApp = new MobileApp();
var webDashboard = new WebDashboard();
var tradingSystem = new TradingSystem();

stock.Attach(mobileApp);
stock.Attach(webDashboard);
stock.Attach(tradingSystem);

stock.SetPrice(150);

Console.WriteLine();

stock.SetPrice(155);

Console.WriteLine();

stock.Detach(webDashboard);

stock.SetPrice(160);

Possible output:

MSFT price changed to $150.00
Mobile App: Stock price updated to $150.00
Web Dashboard: Stock price updated to $150.00
Trading System: Received price $150.00

MSFT price changed to $155.00
Mobile App: Stock price updated to $155.00
Web Dashboard: Stock price updated to $155.00
Trading System: Received price $155.00

MSFT price changed to $160.00
Mobile App: Stock price updated to $160.00
Trading System: Received price $160.00

Notice what happened:

WebDashboard
     |
     ↓
Detach()

It no longer receives notifications.


10. Observer Pattern Using C# Events

In modern C#, we don't always need to manually maintain:

List<IObserver>

C# provides:

  • Delegates

  • Events

These are frequently used to implement observer-like behavior.

For example:

public class Stock
{
    public event EventHandler<StockPriceChangedEventArgs>?
        PriceChanged;

    public decimal Price { get; private set; }

    public void SetPrice(decimal price)
    {
        Price = price;

        PriceChanged?.Invoke(
            this,
            new StockPriceChangedEventArgs(price));
    }
}

Event arguments:

public class StockPriceChangedEventArgs
    : EventArgs
{
    public decimal Price { get; }

    public StockPriceChangedEventArgs(
        decimal price)
    {
        Price = price;
    }
}

Subscribe:

stock.PriceChanged +=
    OnStockPriceChanged;

Handler:

void OnStockPriceChanged(
    object? sender,
    StockPriceChangedEventArgs e)
{
    Console.WriteLine(
        $"Price changed to {e.Price:C}");
}

Unsubscribe:

stock.PriceChanged -=
    OnStockPriceChanged;

This is a very common implementation of observer-style notification in C#.


11. How C# Events Work

The relationship is:

Publisher
    |
    | event
    ↓
Delegate Invocation List
    |
    +---- Subscriber 1
    +---- Subscriber 2
    +---- Subscriber 3

When:

PriceChanged?.Invoke(...);

is executed, subscribed handlers are called.

Therefore:

Event
 ↓
Delegate
 ↓
Multiple Subscribers

This is why understanding the Observer Pattern helps developers understand C# events.


12. IObservable<T> and IObserver<T>

.NET also provides explicit observer abstractions:

IObservable<T>
IObserver<T>

The IObservable<T> represents the producer.

The IObserver<T> represents the subscriber.

The observer interface provides:

void OnNext(T value);

void OnError(Exception error);

void OnCompleted();

For example:

public class StockObserver
    : IObserver<decimal>
{
    public void OnNext(decimal value)
    {
        Console.WriteLine(
            $"Price received: {value:C}");
    }

    public void OnError(Exception error)
    {
        Console.WriteLine(
            $"Error: {error.Message}");
    }

    public void OnCompleted()
    {
        Console.WriteLine(
            "Stock stream completed.");
    }
}

This model is especially useful when working with streams of data and reactive programming concepts.


13. ASP.NET Core Implementation

Let's create a realistic notification example.

Suppose an order changes status:

Order
  ↓
Status Changed
  ↓
Notification Subscribers

We might have:

Order Service
     |
     +---- Email
     |
     +---- SMS
     |
     +---- Push Notification
     |
     +---- Audit Log

14. Order Event

Create an event model:

public record OrderStatusChangedEvent(
    int OrderId,
    string Status);

15. Observer Interface

public interface IOrderObserver
{
    Task UpdateAsync(
        OrderStatusChangedEvent orderEvent);
}

16. Email Observer

public class EmailNotificationObserver
    : IOrderObserver
{
    public Task UpdateAsync(
        OrderStatusChangedEvent orderEvent)
    {
        Console.WriteLine(
            $"Email sent for Order {orderEvent.OrderId}");

        return Task.CompletedTask;
    }
}

17. SMS Observer

public class SmsNotificationObserver
    : IOrderObserver
{
    public Task UpdateAsync(
        OrderStatusChangedEvent orderEvent)
    {
        Console.WriteLine(
            $"SMS sent for Order {orderEvent.OrderId}");

        return Task.CompletedTask;
    }
}

18. Audit Observer

public class AuditObserver
    : IOrderObserver
{
    public Task UpdateAsync(
        OrderStatusChangedEvent orderEvent)
    {
        Console.WriteLine(
            $"Audit entry created for Order " +
            $"{orderEvent.OrderId}");

        return Task.CompletedTask;
    }
}

19. Order Notification Service

public interface IOrderNotificationService
{
    Task NotifyAsync(
        OrderStatusChangedEvent orderEvent);
}

Implementation:

public class OrderNotificationService
    : IOrderNotificationService
{
    private readonly IEnumerable<IOrderObserver>
        _observers;

    public OrderNotificationService(
        IEnumerable<IOrderObserver> observers)
    {
        _observers = observers;
    }

    public async Task NotifyAsync(
        OrderStatusChangedEvent orderEvent)
    {
        foreach (var observer in _observers)
        {
            await observer.UpdateAsync(orderEvent);
        }
    }
}

This is a powerful ASP.NET Core technique.

The service doesn't need to know:

Email
SMS
Audit
Push

It simply receives all registered observers.


20. Register Observers with Dependency Injection

In Program.cs:

builder.Services.AddScoped<
    IOrderObserver,
    EmailNotificationObserver>();

builder.Services.AddScoped<
    IOrderObserver,
    SmsNotificationObserver>();

builder.Services.AddScoped<
    IOrderObserver,
    AuditObserver>();

builder.Services.AddScoped<
    IOrderNotificationService,
    OrderNotificationService>();

When we inject:

IEnumerable<IOrderObserver>

ASP.NET Core provides all registered implementations.

Therefore:

IEnumerable<IOrderObserver>
          |
          +---- EmailNotificationObserver
          +---- SmsNotificationObserver
          +---- AuditObserver

This is an excellent example of combining:

  • Observer Pattern

  • Dependency Injection

  • Open/Closed Principle


21. ASP.NET Core Controller

[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly IOrderNotificationService
        _notificationService;

    public OrdersController(
        IOrderNotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    [HttpPost("{id}/ship")]
    public async Task<IActionResult> ShipOrder(
        int id)
    {
        var orderEvent =
            new OrderStatusChangedEvent(
                id,
                "Shipped");

        await _notificationService
            .NotifyAsync(orderEvent);

        return Ok(new
        {
            OrderId = id,
            Status = "Shipped"
        });
    }
}

The controller doesn't know about:

Email
SMS
Audit

It only interacts with the notification abstraction.


22. Banking Example

Consider a banking application.

A transaction occurs:

$500 deposited

Several components may need to react:

Bank Transaction
       |
       +---- Account Balance
       |
       +---- Email Notification
       |
       +---- SMS Notification
       |
       +---- Fraud Detection
       |
       +---- Audit Log
       |
       +---- Analytics

Instead of putting all logic into the transaction service, observers can subscribe to transaction events.

For example:

public record TransactionCompletedEvent(
    string AccountNumber,
    decimal Amount,
    string TransactionType);

Observers:

EmailObserver
SmsObserver
FraudDetectionObserver
AuditObserver
AnalyticsObserver

This makes the system easier to extend.


23. E-Commerce Order Example

Suppose:

Order #1001
Status = Delivered

Observers might include:

CustomerNotificationObserver
InventoryObserver
AnalyticsObserver
AuditObserver
LoyaltyPointsObserver

Flow:

                 Order Service
                      |
                 Order Delivered
                      |
          +-----------+-----------+
          |           |           |
          ↓           ↓           ↓
      Customer     Inventory   Analytics
      Notify       Update      Update

Adding another subscriber does not necessarily require changing the core order logic.


24. Stock Price Monitoring

Another common example is financial market monitoring.

Stock Price Service
       |
       | Price Changed
       |
       +---- Trading Dashboard
       |
       +---- Mobile Application
       |
       +---- Alert Service
       |
       +---- Analytics

If the price becomes:

$150 → $155

all subscribers can receive the update.


25. Event-Driven Architecture

Observer Pattern concepts are closely related to event-driven architecture, but they are not identical.

A simple in-process observer system:

Object
 ↓
Event
 ↓
Observers

An enterprise event-driven architecture may look like:

Order Service
      |
      | OrderCreated
      ↓
Message Broker
      |
      +---- Notification Service
      |
      +---- Inventory Service
      |
      +---- Analytics Service
      |
      +---- Shipping Service

The important difference is that enterprise systems often use:

  • Message brokers

  • Queues

  • Topics

  • Durable messages

  • Retry policies

  • Dead-letter queues

  • Distributed consumers

Examples include Azure Service Bus, RabbitMQ, Kafka, and other messaging technologies.

The Observer Pattern provides the conceptual foundation for one-to-many notification, while distributed messaging provides a different architectural mechanism for implementing event-driven systems.


26. Observer vs Pub/Sub

These concepts are related but not exactly the same.

ObserverPub/Sub
Subject knows observersPublisher generally doesn't know subscribers
Often in-processCommonly distributed
Direct notificationUsually broker/topic based
Simple communicationSupports larger distributed systems
Often synchronousCan be asynchronous

Observer:

Subject
   |
   +---- Observer A
   +---- Observer B

Pub/Sub:

Publisher
    |
    ↓
Message Broker
    |
    +---- Subscriber A
    +---- Subscriber B

27. Observer vs Mediator

These patterns can look similar.

Observer

Focuses on:

One-to-many notification

Example:

Order
 |
 +---- Email
 +---- SMS
 +---- Audit

Mediator

Focuses on:

Centralized communication between multiple components

Example:

Component A
     |
Component B → Mediator ← Component C
     |
Component D

The Observer pattern is primarily about notification.

Mediator is primarily about coordinating communication.


28. Observer vs Event-Driven Architecture

Observer:

Subject
   ↓
Observers

Event-driven architecture:

Producer
   ↓
Event Broker
   ↓
Consumers

Observer is often an object-level design pattern.

Event-driven architecture is an architectural approach.


29. Advantages of Observer Pattern

1. Loose Coupling

The Subject depends on an abstraction instead of concrete observers.


2. Open/Closed Principle

New observers can often be added without modifying the Subject.

Existing Subject
       +
New Observer

3. Supports One-to-Many Communication

One state change can notify many components.


4. Extensibility

Adding:

Email
SMS
Push
Audit
Analytics

can be straightforward.


5. Separation of Responsibilities

The Subject handles state.

Observers handle their respective reactions.


6. Natural Fit for Notifications

Examples:

Order Status
Stock Price
Bank Transaction
System Alerts

30. Disadvantages of Observer Pattern

1. Too Many Observers

If hundreds of observers are registered, a single notification can trigger substantial work.


2. Notification Order

Unless explicitly designed, observers may not have a guaranteed execution order.


3. Hidden Dependencies

It may not be obvious from reading the Subject that multiple observers will execute when an event occurs.


4. Error Handling

One observer throwing an exception can potentially affect other observers, depending on how notification is implemented.


5. Memory Leaks with Events

Long-lived publishers holding references to subscribers can prevent subscribers from being garbage collected.

This is particularly important in long-running applications.


6. Synchronous Processing Can Become Slow

If every observer executes synchronously:

Subject
 ↓
Observer 1 → 100 ms
 ↓
Observer 2 → 500 ms
 ↓
Observer 3 → 300 ms

the total operation can become slow.

For distributed or long-running work, asynchronous messaging or background processing may be more appropriate.


31. Best Practices

1. Depend on Abstractions

Use:

IOrderObserver

instead of concrete implementations.


2. Keep Observers Focused

An observer should ideally have one clear responsibility.

For example:

EmailObserver

should focus on email notification.


3. Avoid Heavy Synchronous Work

Don't perform long-running operations inside a request thread unnecessarily.

Consider:

Event
 ↓
Queue
 ↓
Background Worker

when appropriate.


4. Handle Observer Failures Carefully

Don't allow one optional observer to unintentionally prevent all other observers from running.


5. Unsubscribe When Necessary

For event-based systems:

publisher.Event -= Handler;

is important when the subscriber's lifetime is shorter than the publisher's lifetime.


6. Use Immutable Event Data

Records are convenient:

public record OrderStatusChangedEvent(
    int OrderId,
    string Status);

The event payload should generally represent a stable fact.


7. Don't Use Observer Everywhere

If a simple direct method call is sufficient, introducing Observer may add unnecessary complexity.


32. Common Mistakes

Mistake 1 – Strong Coupling

Avoid:

public class OrderService
{
    private readonly EmailService _email;
    private readonly SmsService _sms;
    private readonly AuditService _audit;
}

when the business requirement is better represented as extensible notification subscribers.


Mistake 2 – Too Much Logic in the Subject

The Subject should not perform every observer's business responsibility.


Mistake 3 – Forgetting to Unsubscribe

With event-based subscriptions, improper lifetime management can create memory retention problems.


Mistake 4 – Ignoring Exceptions

If multiple observers are notified synchronously, determine what should happen when one fails.


Mistake 5 – Confusing Observer with Distributed Messaging

An in-memory event does not automatically provide:

  • Durability

  • Retry

  • Cross-service communication

  • Guaranteed delivery

  • Dead-letter handling

Those requirements typically need messaging infrastructure.


Mistake 6 – Assuming Notification Order

Don't rely on a specific observer execution order unless the application explicitly guarantees it.


33. Real-World Enterprise Scenarios

The Observer Pattern can be useful in:

Banking

Transaction
 ↓
Notifications
Fraud Detection
Audit
Analytics

E-Commerce

Order Created
 ↓
Inventory
Payment
Notification
Shipping
Analytics

Monitoring

System Alert
 ↓
Email
SMS
Dashboard
Logging

Stock Market Applications

Price Changed
 ↓
Mobile
Web
Trading
Alerts

User Management

User Registered
 ↓
Welcome Email
Audit
Analytics
Profile Initialization

Workflow Systems

Workflow State Changed
 ↓
Notification
Audit
Task Assignment
Analytics

34. Observer Pattern and SOLID

Observer works particularly well with several SOLID principles.

Single Responsibility Principle

Each observer handles one responsibility.

EmailObserver → Email
SmsObserver → SMS
AuditObserver → Audit

Open/Closed Principle

New observers can be introduced without modifying the existing Subject.


Dependency Inversion Principle

The Subject depends on:

IObserver

instead of:

ConcreteObserver

35. Observer Pattern and Dependency Injection

ASP.NET Core makes Observer implementations particularly clean with:

IEnumerable<IOrderObserver>

For example:

public OrderNotificationService(
    IEnumerable<IOrderObserver> observers)
{
    _observers = observers;
}

The service automatically receives all registered implementations.

This means the architecture can evolve from:

Email

to:

Email
SMS
Push
Audit
Analytics

without changing the notification service's core structure.


36. Observer Pattern and Asynchronous Processing

Suppose an order API receives:

POST /api/orders

If the application synchronously performs:

Email
SMS
Push
Analytics
Audit

the API response may become slower.

An alternative is:

API
 ↓
Order Created Event
 ↓
Message Queue
 ↓
Consumers

For example:

                Order Service
                     |
                     ↓
              OrderCreated
                     |
                     ↓
                Message Broker
            /        |        \
           /         |         \
          ↓          ↓          ↓
       Email      Inventory   Analytics
       Service     Service     Service

This is no longer simply an in-process Observer Pattern; it becomes distributed event-driven architecture.


37. Observer Pattern in Modern .NET

There are several ways to implement Observer-like behavior in .NET:

Option 1 – Interfaces

IObserver

Useful for explicit pattern implementation.

Option 2 – Events

event EventHandler

Very common for in-process notifications.

Option 3 – IObservable<T>

Useful for observable streams.

Option 4 – Application Events

Useful for decoupling application components.

Option 5 – Domain Events

Useful for business events inside domain-driven applications.

Option 6 – Message Brokers

Useful for distributed event-driven systems.


38. Interview Questions

Beginner

1. What is the Observer Design Pattern?

The Observer Pattern defines a one-to-many dependency where observers are notified when the subject changes.


2. What are the main components?

Subject
Observer
Concrete Subject
Concrete Observer

3. What is a Subject?

The object whose state changes and which notifies observers.


4. What is an Observer?

An object that receives notifications and reacts to changes.


5. What problem does Observer solve?

It reduces coupling between an object that generates changes and objects that need to react to those changes.


Intermediate

6. How is Observer implemented in C#?

Common approaches include:

Interfaces
Delegates
Events
IObservable<T>

7. How do C# events relate to Observer?

C# events provide a built-in mechanism for one-to-many notification and are frequently used to implement Observer-style behavior.


8. What is IObservable<T>?

It represents a producer of observable values that can be subscribed to by implementations of IObserver<T>.


9. What are the methods in IObserver<T>?

OnNext()
OnError()
OnCompleted()

10. What is the difference between Observer and Pub/Sub?

Observer usually represents direct object-level notification, while Pub/Sub commonly introduces an intermediary such as a message broker.


39. Advanced Interview Questions

11. What is the difference between Observer and Mediator?

Observer focuses on one-to-many notification.

Mediator centralizes communication and coordination between multiple components.


12. Can Observer be asynchronous?

Yes.

Observers can expose:

Task UpdateAsync(...)

or an asynchronous event-processing architecture can be introduced.


13. What happens if one observer throws an exception?

It depends on the implementation.

A robust notification system should define whether:

  • Processing stops

  • Other observers continue

  • Errors are logged

  • Failed work is retried


14. Can Observer cause memory leaks?

Yes, especially with events.

A long-lived publisher can keep references to subscribers that should otherwise be eligible for garbage collection.


15. How can you avoid event-related memory leaks?

Unsubscribe appropriately:

publisher.Event -= Handler;

Also consider subscription lifetime and ownership carefully.


16. How would you implement Observer in ASP.NET Core?

One approach is:

IObserver
     ↓
Multiple Implementations
     ↓
Dependency Injection
     ↓
IEnumerable<IObserver>

17. Is Observer suitable for microservices?

The Observer concept can inspire event-driven communication, but direct in-memory Observer implementations generally do not provide reliable cross-service communication.

For microservices, consider:

  • Message brokers

  • Events

  • Queues

  • Topics

  • Durable messaging


18. What is a Domain Event?

A domain event represents something meaningful that happened within the business domain.

For example:

OrderPlaced
PaymentCompleted
OrderShipped
CustomerRegistered

Observers or event handlers can react to those events.


19. What is the difference between an Event and an Event Handler?

The event represents something that happened.

The event handler contains the logic that reacts to that event.

Event
 ↓
Handler

20. When should you avoid Observer?

Avoid it when:

  • There are only two tightly related components.

  • A direct method call is simpler.

  • Notification behavior is difficult to understand.

  • The observer chain becomes excessively complicated.

  • A distributed messaging solution is actually required.


40. Practical Enterprise Architecture

A modern .NET application might evolve like this:

                   Order Service
                        |
                        ↓
                 OrderPlaced Event
                        |
            +-----------+-----------+
            |           |           |
            ↓           ↓           ↓
         Email       Audit       Analytics
       Observer     Observer      Observer

For a distributed architecture:

                   Order Service
                        |
                        ↓
                  OrderPlaced
                        |
                        ↓
                 Message Broker
               /        |        \
              /         |         \
             ↓          ↓          ↓
        Notification  Inventory  Analytics
           Service      Service     Service

The first is an in-process Observer-style design.

The second is an event-driven distributed architecture.


41. Observer Pattern – Complete Conceptual Flow

The complete lifecycle is:

1. Create Subject
        ↓
2. Create Observers
        ↓
3. Attach / Subscribe
        ↓
4. Subject State Changes
        ↓
5. Subject Raises Notification
        ↓
6. Observers Receive Notification
        ↓
7. Each Observer Performs Its Responsibility
        ↓
8. Observer Can Unsubscribe

In code:

subject.Attach(observer);

subject.ChangeState();

subject.Notify();

subject.Detach(observer);

Or with C# events:

publisher.Event += Handler;

publisher.ChangeState();

publisher.Event -= Handler;

42. Key Takeaways

Remember these important points:

1. Observer is a Behavioral Design Pattern

It focuses on communication between objects.

2. It represents a one-to-many relationship

One Subject
     ↓
Many Observers

3. It reduces coupling

The Subject does not need to know the concrete implementation of observers.

4. C# events are closely related to Observer

event EventHandler

is a common way to implement one-to-many notification.

5. IObservable<T> is another .NET mechanism

It provides an explicit observable/subscriber model.

6. Dependency Injection works very well with Observer

ASP.NET Core can inject:

IEnumerable<IOrderObserver>

to provide multiple observers.

7. Observer is not the same as Pub/Sub

Observer is usually direct object-level notification.

Pub/Sub typically uses an intermediary.

8. Observer is not the same as distributed event-driven architecture

For microservices, reliable communication usually requires messaging infrastructure.

9. Watch observer lifetime

Especially when using C# events.

10. Keep observers focused

Each observer should ideally have a clear responsibility.


Conclusion

The Observer Design Pattern is one of the most practical behavioral patterns for modern software development.

It provides a clean way to establish:

One-to-Many Communication

without tightly coupling the publisher to every consumer.

The basic architecture is:

                    Subject
                       |
                  State Changed
                       |
              +--------+--------+
              |        |        |
              ↓        ↓        ↓
           Observer Observer Observer

In C#, this concept appears naturally through:

Interfaces
Delegates
Events
IObservable<T>

In ASP.NET Core applications, it can be combined with:

Dependency Injection
Domain Events
Application Events
Notification Handlers

And in distributed systems, the same fundamental one-to-many notification concept can evolve into:

Producer
   ↓
Event / Message
   ↓
Message Broker
   ↓
Multiple Consumers

The most important lesson is:

Use the Observer Pattern when multiple components need to react to a change without making the component that produces the change tightly coupled to every consumer.

It is particularly valuable for notification systems, order processing, stock monitoring, banking events, UI updates, audit logging, analytics, and event-driven application designs.


🚀 Coming Up Next: Part 4.8 – State Design Pattern

In the next article, we'll explore the State Design Pattern, including:

  • What is the State Pattern?

  • Why do we need it?

  • State-dependent behavior

  • Eliminating large if-else and switch statements

  • Context and State concepts

  • UML Class Diagram

  • Complete C# Console Application

  • Order processing example

  • Banking account state example

  • ASP.NET Core implementation

  • State transitions

  • State machines

  • Real-world enterprise examples

  • State vs Strategy

  • State vs Command

  • State vs Chain of Responsibility

  • Advantages and disadvantages

  • Best practices

  • Common mistakes

  • Interview questions

The State Pattern is particularly useful in enterprise applications where an object's behavior changes significantly based on its current state—for example, Order Pending → Confirmed → Shipped → Delivered → Cancelled.

Don't Copy

Protected by Copyscape Online Plagiarism Checker