State Pattern: Elegant State Machines
design-patternsbehavioralstate

What is the State Pattern?
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class, delegating operations to the current state object.
stateDiagram-v2
[*] --> Draft
Draft --> Pending : confirm()
Pending --> Paid : pay()
Pending --> Cancelled : cancel()
Paid --> Shipped : ship()
Paid --> Cancelled : cancel()
Shipped --> Delivered : deliver()
Delivered --> [*]
Cancelled --> [*]When to Use State
- When an object has behavior that depends on its state
- When you have multiple conditionals that depend on object state
- When you need a state machine with well-defined transitions
- When you want to avoid code with many if/else or switch based on state
The Problem
// Without State Pattern - Code with multiple conditionals
class Order {
status: 'draft' | 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled';
confirm(): void {
if (this.status === 'draft') {
this.status = 'pending';
} else {
throw new Error(`Cannot confirm order in ${this.status} state`);
}
}
pay(): void {
if (this.status === 'pending') {
this.status = 'paid';
} else {
throw new Error(`Cannot pay order in ${this.status} state`);
}
}
ship(): void {
if (this.status === 'paid') {
this.status = 'shipped';
} else {
throw new Error(`Cannot ship order in ${this.status} state`);
}
}
// ... more methods with more conditionals
}The Solution: State Pattern
1. Define the State Interface
interface OrderState {
readonly name: string;
confirm(order: Order): Promise<void>;
pay(order: Order, paymentId: string): Promise<void>;
ship(order: Order, trackingNumber: string): Promise<void>;
deliver(order: Order): Promise<void>;
cancel(order: Order, reason: string): Promise<void>;
refund(order: Order): Promise<void>;
}2. Create Base Class with Default Behavior
abstract class BaseOrderState implements OrderState {
abstract readonly name: string;
async confirm(_order: Order): Promise<void> {
throw new Error(`Cannot confirm order in ${this.name} state`);
}
async pay(_order: Order, _paymentId: string): Promise<void> {
throw new Error(`Cannot pay order in ${this.name} state`);
}
async ship(_order: Order, _trackingNumber: string): Promise<void> {
throw new Error(`Cannot ship order in ${this.name} state`);
}
async deliver(_order: Order): Promise<void> {
throw new Error(`Cannot deliver order in ${this.name} state`);
}
async cancel(_order: Order, _reason: string): Promise<void> {
throw new Error(`Cannot cancel order in ${this.name} state`);
}
async refund(_order: Order): Promise<void> {
throw new Error(`Cannot refund order in ${this.name} state`);
}
}3. Implement Concrete States
Each state only implements valid transitions:
/** Initial state - can confirm or cancel */
class DraftState extends BaseOrderState {
readonly name = 'draft';
async confirm(order: Order): Promise<void> {
console.log('Order confirmed, awaiting payment');
order.setState(new PendingPaymentState());
}
async cancel(order: Order, reason: string): Promise<void> {
console.log(`Order cancelled: ${reason}`);
order.setState(new CancelledState());
}
}
/** Awaiting payment - can pay or cancel */
class PendingPaymentState extends BaseOrderState {
readonly name = 'pending_payment';
async pay(order: Order, paymentId: string): Promise<void> {
console.log(`Payment received: ${paymentId}`);
order.context.paymentId = paymentId;
order.setState(new PaidState());
}
async cancel(order: Order, reason: string): Promise<void> {
console.log(`Order cancelled: ${reason}`);
order.setState(new CancelledState());
}
}
/** Paid - can ship or refund */
class PaidState extends BaseOrderState {
readonly name = 'paid';
async ship(order: Order, trackingNumber: string): Promise<void> {
console.log(`Order shipped: ${trackingNumber}`);
order.context.trackingNumber = trackingNumber;
order.setState(new ShippedState());
}
async refund(order: Order): Promise<void> {
console.log('Processing refund...');
order.setState(new RefundedState());
}
}
/** Shipped - can deliver */
class ShippedState extends BaseOrderState {
readonly name = 'shipped';
async deliver(order: Order): Promise<void> {
console.log('Order delivered!');
order.setState(new DeliveredState());
}
}
/** Delivered - can refund */
class DeliveredState extends BaseOrderState {
readonly name = 'delivered';
async refund(order: Order): Promise<void> {
console.log('Processing refund for delivered order...');
order.setState(new RefundedState());
}
}
/** Terminal states */
class CancelledState extends BaseOrderState {
readonly name = 'cancelled';
}
class RefundedState extends BaseOrderState {
readonly name = 'refunded';
}4. Create the Context (Order)
interface OrderContext {
orderId: string;
items: Array<{ productId: string; quantity: number }>;
total: number;
paymentId?: string;
trackingNumber?: string;
}
class Order {
private state: OrderState = new DraftState();
private history: Array<{ state: string; timestamp: Date }> = [];
public context: OrderContext;
constructor(orderId: string) {
this.context = {
orderId,
items: [],
total: 0,
};
this.recordHistory();
}
setState(state: OrderState): void {
this.state = state;
this.recordHistory();
}
private recordHistory(): void {
this.history.push({
state: this.state.name,
timestamp: new Date(),
});
}
getHistory() {
return [...this.history];
}
getCurrentState(): string {
return this.state.name;
}
// Delegate to current state
confirm(): Promise<void> {
return this.state.confirm(this);
}
pay(paymentId: string): Promise<void> {
return this.state.pay(this, paymentId);
}
ship(trackingNumber: string): Promise<void> {
return this.state.ship(this, trackingNumber);
}
deliver(): Promise<void> {
return this.state.deliver(this);
}
cancel(reason: string): Promise<void> {
return this.state.cancel(this, reason);
}
refund(): Promise<void> {
return this.state.refund(this);
}
}Usage Example
const order = new Order('ORD-001');
// Normal flow
await order.confirm(); // draft -> pending_payment
await order.pay('PAY-123'); // pending_payment -> paid
await order.ship('TRACK-456'); // paid -> shipped
await order.deliver(); // shipped -> delivered
// State history
order.getHistory().forEach(h => {
console.log(`${h.state} at ${h.timestamp.toISOString()}`);
});
// Try invalid transition
try {
await order.cancel('Changed mind');
} catch (error) {
console.log(error.message); // Cannot cancel order in delivered state
}State Diagram
stateDiagram-v2
[*] --> Draft
Draft --> PendingPayment: confirm()
Draft --> Cancelled: cancel()
PendingPayment --> Paid: pay()
PendingPayment --> Cancelled: cancel()
Paid --> Shipped: ship()
Paid --> Refunded: refund()
Shipped --> Delivered: deliver()
Delivered --> Refunded: refund()
Cancelled --> [*]
Refunded --> [*]Summary
| Aspect | Description |
|---|---|
| Purpose | Change behavior based on internal state |
| Problem | Complex conditional logic based on state |
| Solution | Encapsulate each state in a separate class |
| Benefit | Single Responsibility - each state handles its logic |
Conclusion
The State pattern is ideal for modeling complex state machines in a clean and maintainable way. Each state encapsulates its own logic, making code easier to understand and extend.
Based on "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).