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
Introduction
What is the State Design Pattern?
Why Do We Need the State Pattern?
The Problem with Large
if-elseandswitchStatementsReal-World Analogy
State Pattern Terminology
How the State Pattern Works
UML Class Diagram
Components of the State Pattern
Complete C# Console Application
Order Processing Example
State Transitions
ASP.NET Core Implementation
Banking Account Example
E-Commerce Example
State Pattern and State Machines
State vs Strategy Pattern
State vs Command Pattern
State vs Chain of Responsibility
Advantages
Disadvantages
Best Practices
Common Mistakes
Real-World Enterprise Scenarios
Interview Questions
Key Takeaways
Conclusion
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-elseorswitchstatements 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
switchis 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-elseandswitchstatementsStrategy 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.

No comments:
Post a Comment