Allow an object to alter its behavior when its internal state changes.
The Problem
An order goes through multiple states (pending, confirmed, shipped, delivered, cancelled) with conditional logic in every method. Each operation must check the current state and behave differently: confirming a pending order succeeds, but confirming a shipped order fails. Scattering these conditionals throughout the code makes it fragile and hard to extend.
The Solution
The State pattern delegates state-specific behavior to individual state objects. Each state knows which transitions are valid and throws errors for invalid ones. The context (OrderContext) holds the current state and delegates operations to it.
Structure
State (interface OrderState) — Declares confirm(), ship(), deliver(), cancel().
Context (OrderContext) — Holds the current state and an audit trail of transitions.
Concrete States — PendingState, ConfirmedState, ShippedState, DeliveredState, CancelledState.
import { OrderState, OrderStateContext, StateTransitionResult } from './order-state.interface';import { ConfirmedState } from './confirmed.state';import { CancelledState } from './cancelled.state';export class PendingState implements OrderState { getName(): string { return 'Pending'; } confirm(context: OrderStateContext): StateTransitionResult { context.setState(new ConfirmedState()); return { success: true, message: 'Order confirmed successfully', newState: 'Confirmed', }; } ship(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot ship from Pending state — order must be confirmed first', newState: this.getName(), }; } deliver(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot deliver from Pending state', newState: this.getName(), }; } cancel(context: OrderStateContext): StateTransitionResult { context.setState(new CancelledState()); return { success: true, message: 'Order cancelled from pending state', newState: 'Cancelled', }; }}
import { OrderState, OrderStateContext, StateTransitionResult } from './order-state.interface';import { ShippedState } from './shipped.state';import { CancelledState } from './cancelled.state';export class ConfirmedState implements OrderState { getName(): string { return 'Confirmed'; } confirm(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Order is already confirmed', newState: this.getName(), }; } ship(context: OrderStateContext): StateTransitionResult { context.setState(new ShippedState()); return { success: true, message: 'Order shipped successfully', newState: 'Shipped', }; } deliver(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot deliver from Confirmed state — order must be shipped first', newState: this.getName(), }; } cancel(context: OrderStateContext): StateTransitionResult { context.setState(new CancelledState()); return { success: true, message: 'Order cancelled from confirmed state — refund will be processed', newState: 'Cancelled', }; }}
import { OrderState, OrderStateContext, StateTransitionResult } from './order-state.interface';import { DeliveredState } from './delivered.state';export class ShippedState implements OrderState { getName(): string { return 'Shipped'; } confirm(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot confirm from Shipped state — order is already in transit', newState: this.getName(), }; } ship(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Order is already shipped', newState: this.getName(), }; } deliver(context: OrderStateContext): StateTransitionResult { context.setState(new DeliveredState()); return { success: true, message: 'Order delivered successfully', newState: 'Delivered', }; } cancel(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot cancel from Shipped state — order is already in transit', newState: this.getName(), }; }}
import { OrderState, OrderStateContext, StateTransitionResult } from './order-state.interface';export class DeliveredState implements OrderState { getName(): string { return 'Delivered'; } confirm(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot confirm from Delivered state — order is already delivered', newState: this.getName(), }; } ship(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot ship from Delivered state — order is already delivered', newState: this.getName(), }; } deliver(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Order is already delivered', newState: this.getName(), }; } cancel(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot cancel from Delivered state — order is already delivered', newState: this.getName(), }; }}
import { OrderState, OrderStateContext, StateTransitionResult } from './order-state.interface';export class CancelledState implements OrderState { getName(): string { return 'Cancelled'; } confirm(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot confirm from Cancelled state — order has been cancelled', newState: this.getName(), }; } ship(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot ship from Cancelled state — order has been cancelled', newState: this.getName(), }; } deliver(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Cannot deliver from Cancelled state — order has been cancelled', newState: this.getName(), }; } cancel(_context: OrderStateContext): StateTransitionResult { return { success: false, message: 'Order is already cancelled', newState: this.getName(), }; }}
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';import { OrderContext } from './order-context';@Injectable()export class StateService { private readonly orders = new Map<string, OrderContext>(); createOrder(id?: string): OrderContext { const orderId = id ?? `ORD_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; if (this.orders.has(orderId)) { throw new BadRequestException(`Order ${orderId} already exists`); } const order = new OrderContext(orderId); this.orders.set(orderId, order); return order; } getOrder(id: string): OrderContext { const order = this.orders.get(id); if (!order) { throw new NotFoundException(`Order ${id} not found`); } return order; } transition(id: string, action: string) { const order = this.getOrder(id); switch (action) { case 'confirm': return order.confirm(); case 'ship': return order.ship(); case 'deliver': return order.deliver(); case 'cancel': return order.cancel(); default: throw new BadRequestException( `Unknown action "${action}". Valid actions: confirm, ship, deliver, cancel`, ); } }}
NestJS Integration
The StateService is an @Injectable() singleton that manages a Map of order contexts. Individual state objects are plain classes created on demand — they are lightweight and stateless, so they don’t need to be NestJS providers. The context and state objects form a self-contained state machine that the service exposes via simple methods.
When to Use
An object’s behavior depends on its current state and must change at runtime.
Operations have massive conditional logic based on the object’s state.
You want to make state transitions explicit and enforce valid transition rules.
When NOT to Use
The object has only two or three states with simple transitions — a boolean or enum with a switch statement is clearer.
State transitions are rare and the overhead of state objects is not justified.