What is State Pattern?

The State pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. It appears as if the object changed its class. Think of it like a vending machine - it behaves differently when it’s waiting for coins, has coins inserted, is dispensing a product, or is out of stock. Each state has its own set of valid operations and transitions.

I’ll demonstrate how this pattern can help you build clean, maintainable state machines in Go applications.

Let’s start with a scenario: Order Processing System

Imagine you’re building an e-commerce order processing system where orders go through different states: Pending, Confirmed, Shipped, Delivered, and Cancelled. Each state has different allowed operations and business rules. For example, you can only ship a confirmed order, and you can only cancel orders that haven’t been shipped yet.

Without State Pattern

Here’s how you might handle this without the State pattern:

type OrderStatus int

const (
    Pending OrderStatus = iota
    Confirmed
    Shipped
    Delivered
    Cancelled
)

type Order struct {
    ID       string
    Status   OrderStatus
    Items    []string
    Total    float64
    Address  string
}

func (o *Order) Confirm() error {
    // Complex conditional logic for state transitions
    switch o.Status {
    case Pending:
        o.Status = Confirmed
        log.Printf("Order %s confirmed", o.ID)
        return nil
    case Confirmed:
        return errors.New("order already confirmed")
    case Shipped:
        return errors.New("cannot confirm shipped order")
    case Delivered:
        return errors.New("cannot confirm delivered order")
    case Cancelled:
        return errors.New("cannot confirm cancelled order")
    default:
        return errors.New("invalid order status")
    }
}

func (o *Order) Ship() error {
    // More complex conditional logic
    switch o.Status {
    case Pending:
        return errors.New("cannot ship pending order")
    case Confirmed:
        o.Status = Shipped
        log.Printf("Order %s shipped", o.ID)
        return nil
    case Shipped:
        return errors.New("order already shipped")
    case Delivered:
        return errors.New("cannot ship delivered order")
    case Cancelled:
        return errors.New("cannot ship cancelled order")
    default:
        return errors.New("invalid order status")
    }
}

func (o *Order) Cancel() error {
    // Even more conditional logic
    switch o.Status {
    case Pending, Confirmed:
        o.Status = Cancelled
        log.Printf("Order %s cancelled", o.ID)
        return nil
    case Shipped:
        return errors.New("cannot cancel shipped order")
    case Delivered:
        return errors.New("cannot cancel delivered order")
    case Cancelled:
        return errors.New("order already cancelled")
    default:
        return errors.New("invalid order status")
    }
}

// This approach becomes unwieldy as states and transitions grow
// Adding new states requires modifying all methods
// Business logic is scattered across multiple switch statements

With State Pattern

Let’s refactor this using the State pattern:

package main

import (
    "fmt"
    "log"
    "time"
)

// State interface defines the contract for all states
type OrderState interface {
    Confirm(order *Order) error
    Ship(order *Order) error
    Deliver(order *Order) error
    Cancel(order *Order) error
    GetStatus() string
    GetAllowedActions() []string
}

// Context - the Order that changes state
type Order struct {
    ID          string
    Items       []string
    Total       float64
    Address     string
    CreatedAt   time.Time
    UpdatedAt   time.Time
    state       OrderState
    StateHistory []StateTransition
}

type StateTransition struct {
    FromState string
    ToState   string
    Timestamp time.Time
    Action    string
}

func NewOrder(id string, items []string, total float64, address string) *Order {
    order := &Order{
        ID:           id,
        Items:        items,
        Total:        total,
        Address:      address,
        CreatedAt:    time.Now(),
        UpdatedAt:    time.Now(),
        StateHistory: make([]StateTransition, 0),
    }
    
    // Start in pending state
    order.setState(&PendingState{})
    return order
}

func (o *Order) setState(state OrderState) {
    oldState := ""
    if o.state != nil {
        oldState = o.state.GetStatus()
    }
    
    o.state = state
    o.UpdatedAt = time.Now()
    
    // Record state transition
    transition := StateTransition{
        FromState: oldState,
        ToState:   state.GetStatus(),
        Timestamp: time.Now(),
    }
    o.StateHistory = append(o.StateHistory, transition)
    
    log.Printf("Order %s transitioned from %s to %s", o.ID, oldState, state.GetStatus())
}

// Delegate methods to current state
func (o *Order) Confirm() error {
    return o.state.Confirm(o)
}

func (o *Order) Ship() error {
    return o.state.Ship(o)
}

func (o *Order) Deliver() error {
    return o.state.Deliver(o)
}

func (o *Order) Cancel() error {
    return o.state.Cancel(o)
}

func (o *Order) GetStatus() string {
    return o.state.GetStatus()
}

func (o *Order) GetAllowedActions() []string {
    return o.state.GetAllowedActions()
}

func (o *Order) GetStateHistory() []StateTransition {
    return o.StateHistory
}

// Concrete States

// Pending State
type PendingState struct{}

func (s *PendingState) Confirm(order *Order) error {
    log.Printf("Confirming order %s", order.ID)
    order.setState(&ConfirmedState{})
    return nil
}

func (s *PendingState) Ship(order *Order) error {
    return fmt.Errorf("cannot ship order %s: order is still pending", order.ID)
}

func (s *PendingState) Deliver(order *Order) error {
    return fmt.Errorf("cannot deliver order %s: order is still pending", order.ID)
}

func (s *PendingState) Cancel(order *Order) error {
    log.Printf("Cancelling pending order %s", order.ID)
    order.setState(&CancelledState{})
    return nil
}

func (s *PendingState) GetStatus() string {
    return "PENDING"
}

func (s *PendingState) GetAllowedActions() []string {
    return []string{"confirm", "cancel"}
}

// Confirmed State
type ConfirmedState struct{}

func (s *ConfirmedState) Confirm(order *Order) error {
    return fmt.Errorf("order %s is already confirmed", order.ID)
}

func (s *ConfirmedState) Ship(order *Order) error {
    log.Printf("Shipping confirmed order %s", order.ID)
    order.setState(&ShippedState{})
    return nil
}

func (s *ConfirmedState) Deliver(order *Order) error {
    return fmt.Errorf("cannot deliver order %s: order must be shipped first", order.ID)
}

func (s *ConfirmedState) Cancel(order *Order) error {
    log.Printf("Cancelling confirmed order %s", order.ID)
    order.setState(&CancelledState{})
    return nil
}

func (s *ConfirmedState) GetStatus() string {
    return "CONFIRMED"
}

func (s *ConfirmedState) GetAllowedActions() []string {
    return []string{"ship", "cancel"}
}

// Shipped State
type ShippedState struct{}

func (s *ShippedState) Confirm(order *Order) error {
    return fmt.Errorf("order %s is already confirmed and shipped", order.ID)
}

func (s *ShippedState) Ship(order *Order) error {
    return fmt.Errorf("order %s is already shipped", order.ID)
}

func (s *ShippedState) Deliver(order *Order) error {
    log.Printf("Delivering shipped order %s", order.ID)
    order.setState(&DeliveredState{})
    return nil
}

func (s *ShippedState) Cancel(order *Order) error {
    return fmt.Errorf("cannot cancel order %s: order has already been shipped", order.ID)
}

func (s *ShippedState) GetStatus() string {
    return "SHIPPED"
}

func (s *ShippedState) GetAllowedActions() []string {
    return []string{"deliver"}
}

// Delivered State
type DeliveredState struct{}

func (s *DeliveredState) Confirm(order *Order) error {
    return fmt.Errorf("order %s is already delivered", order.ID)
}

func (s *DeliveredState) Ship(order *Order) error {
    return fmt.Errorf("order %s is already delivered", order.ID)
}

func (s *DeliveredState) Deliver(order *Order) error {
    return fmt.Errorf("order %s is already delivered", order.ID)
}

func (s *DeliveredState) Cancel(order *Order) error {
    return fmt.Errorf("cannot cancel order %s: order has been delivered", order.ID)
}

func (s *DeliveredState) GetStatus() string {
    return "DELIVERED"
}

func (s *DeliveredState) GetAllowedActions() []string {
    return []string{} // No actions allowed in delivered state
}

// Cancelled State
type CancelledState struct{}

func (s *CancelledState) Confirm(order *Order) error {
    return fmt.Errorf("cannot confirm order %s: order has been cancelled", order.ID)
}

func (s *CancelledState) Ship(order *Order) error {
    return fmt.Errorf("cannot ship order %s: order has been cancelled", order.ID)
}

func (s *CancelledState) Deliver(order *Order) error {
    return fmt.Errorf("cannot deliver order %s: order has been cancelled", order.ID)
}

func (s *CancelledState) Cancel(order *Order) error {
    return fmt.Errorf("order %s is already cancelled", order.ID)
}

func (s *CancelledState) GetStatus() string {
    return "CANCELLED"
}

func (s *CancelledState) GetAllowedActions() []string {
    return []string{} // No actions allowed in cancelled state
}

// Order Manager for handling multiple orders
type OrderManager struct {
    orders map[string]*Order
}

func NewOrderManager() *OrderManager {
    return &OrderManager{
        orders: make(map[string]*Order),
    }
}

func (om *OrderManager) CreateOrder(id string, items []string, total float64, address string) *Order {
    order := NewOrder(id, items, total, address)
    om.orders[id] = order
    return order
}

func (om *OrderManager) GetOrder(id string) (*Order, error) {
    order, exists := om.orders[id]
    if !exists {
        return nil, fmt.Errorf("order %s not found", id)
    }
    return order, nil
}

func (om *OrderManager) GetOrdersByStatus(status string) []*Order {
    var result []*Order
    for _, order := range om.orders {
        if order.GetStatus() == status {
            result = append(result, order)
        }
    }
    return result
}

func (om *OrderManager) ProcessOrder(id string, action string) error {
    order, err := om.GetOrder(id)
    if err != nil {
        return err
    }
    
    switch action {
    case "confirm":
        return order.Confirm()
    case "ship":
        return order.Ship()
    case "deliver":
        return order.Deliver()
    case "cancel":
        return order.Cancel()
    default:
        return fmt.Errorf("unknown action: %s", action)
    }
}

func main() {
    fmt.Println("=== Order Processing with State Pattern ===")
    
    // Create order manager
    manager := NewOrderManager()
    
    // Create some orders
    order1 := manager.CreateOrder("ORD-001", []string{"Laptop", "Mouse"}, 1299.99, "123 Main St")
    order2 := manager.CreateOrder("ORD-002", []string{"Book", "Pen"}, 25.50, "456 Oak Ave")
    
    // Demonstrate normal order flow
    fmt.Println("\n--- Normal Order Flow ---")
    fmt.Printf("Order %s status: %s, allowed actions: %v\n", 
        order1.ID, order1.GetStatus(), order1.GetAllowedActions())
    
    // Confirm order
    err := order1.Confirm()
    if err != nil {
        log.Printf("Error: %v", err)
    }
    fmt.Printf("Order %s status: %s, allowed actions: %v\n", 
        order1.ID, order1.GetStatus(), order1.GetAllowedActions())
    
    // Ship order
    err = order1.Ship()
    if err != nil {
        log.Printf("Error: %v", err)
    }
    fmt.Printf("Order %s status: %s, allowed actions: %v\n", 
        order1.ID, order1.GetStatus(), order1.GetAllowedActions())
    
    // Deliver order
    err = order1.Deliver()
    if err != nil {
        log.Printf("Error: %v", err)
    }
    fmt.Printf("Order %s status: %s, allowed actions: %v\n", 
        order1.ID, order1.GetStatus(), order1.GetAllowedActions())
    
    // Demonstrate invalid transitions
    fmt.Println("\n--- Testing Invalid Transitions ---")
    
    // Try to ship again (should fail)
    err = order1.Ship()
    if err != nil {
        fmt.Printf("Expected error: %v\n", err)
    }
    
    // Try to cancel delivered order (should fail)
    err = order1.Cancel()
    if err != nil {
        fmt.Printf("Expected error: %v\n", err)
    }
    
    // Demonstrate cancellation flow
    fmt.Println("\n--- Cancellation Flow ---")
    fmt.Printf("Order %s status: %s\n", order2.ID, order2.GetStatus())
    
    err = order2.Confirm()
    if err != nil {
        log.Printf("Error: %v", err)
    }
    fmt.Printf("Order %s status: %s\n", order2.ID, order2.GetStatus())
    
    err = order2.Cancel()
    if err != nil {
        log.Printf("Error: %v", err)
    }
    fmt.Printf("Order %s status: %s, allowed actions: %v\n", 
        order2.ID, order2.GetStatus(), order2.GetAllowedActions())
    
    // Show state history
    fmt.Println("\n--- Order State History ---")
    for _, order := range []*Order{order1, order2} {
        fmt.Printf("\nOrder %s history:\n", order.ID)
        for _, transition := range order.GetStateHistory() {
            fmt.Printf("  %s: %s -> %s\n", 
                transition.Timestamp.Format("15:04:05"), 
                transition.FromState, 
                transition.ToState)
        }
    }
    
    // Show orders by status
    fmt.Println("\n--- Orders by Status ---")
    statuses := []string{"PENDING", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED"}
    for _, status := range statuses {
        orders := manager.GetOrdersByStatus(status)
        fmt.Printf("%s orders: %d\n", status, len(orders))
        for _, order := range orders {
            fmt.Printf("  - %s\n", order.ID)
        }
    }
}

Advanced State Pattern with State Machine

Here’s a more advanced implementation with a formal state machine:

package main

import (
    "fmt"
    "sync"
)

// State machine definition
type StateMachine struct {
    currentState string
    states       map[string]State
    transitions  map[string]map[string]string // from -> action -> to
    mutex        sync.RWMutex
}

type State interface {
    OnEnter(context interface{}) error
    OnExit(context interface{}) error
    GetName() string
}

func NewStateMachine(initialState string) *StateMachine {
    return &StateMachine{
        currentState: initialState,
        states:       make(map[string]State),
        transitions:  make(map[string]map[string]string),
    }
}

func (sm *StateMachine) AddState(state State) {
    sm.mutex.Lock()
    defer sm.mutex.Unlock()
    sm.states[state.GetName()] = state
}

func (sm *StateMachine) AddTransition(from, action, to string) {
    sm.mutex.Lock()
    defer sm.mutex.Unlock()
    
    if sm.transitions[from] == nil {
        sm.transitions[from] = make(map[string]string)
    }
    sm.transitions[from][action] = to
}

func (sm *StateMachine) Trigger(action string, context interface{}) error {
    sm.mutex.Lock()
    defer sm.mutex.Unlock()
    
    // Check if transition is valid
    if sm.transitions[sm.currentState] == nil {
        return fmt.Errorf("no transitions defined for state %s", sm.currentState)
    }
    
    nextState, exists := sm.transitions[sm.currentState][action]
    if !exists {
        return fmt.Errorf("invalid action %s for state %s", action, sm.currentState)
    }
    
    // Get current and next state objects
    currentStateObj := sm.states[sm.currentState]
    nextStateObj := sm.states[nextState]
    
    if currentStateObj == nil || nextStateObj == nil {
        return fmt.Errorf("state object not found")
    }
    
    // Execute transition
    if err := currentStateObj.OnExit(context); err != nil {
        return fmt.Errorf("failed to exit state %s: %w", sm.currentState, err)
    }
    
    oldState := sm.currentState
    sm.currentState = nextState
    
    if err := nextStateObj.OnEnter(context); err != nil {
        // Rollback state change
        sm.currentState = oldState
        return fmt.Errorf("failed to enter state %s: %w", nextState, err)
    }
    
    return nil
}

func (sm *StateMachine) GetCurrentState() string {
    sm.mutex.RLock()
    defer sm.mutex.RUnlock()
    return sm.currentState
}

func (sm *StateMachine) GetValidActions() []string {
    sm.mutex.RLock()
    defer sm.mutex.RUnlock()
    
    var actions []string
    if transitions := sm.transitions[sm.currentState]; transitions != nil {
        for action := range transitions {
            actions = append(actions, action)
        }
    }
    return actions
}

// Example states for a document workflow
type DraftState struct{}

func (s *DraftState) OnEnter(context interface{}) error {
    fmt.Println("Document entered DRAFT state")
    return nil
}

func (s *DraftState) OnExit(context interface{}) error {
    fmt.Println("Document exiting DRAFT state")
    return nil
}

func (s *DraftState) GetName() string {
    return "DRAFT"
}

type ReviewState struct{}

func (s *ReviewState) OnEnter(context interface{}) error {
    fmt.Println("Document entered REVIEW state")
    return nil
}

func (s *ReviewState) OnExit(context interface{}) error {
    fmt.Println("Document exiting REVIEW state")
    return nil
}

func (s *ReviewState) GetName() string {
    return "REVIEW"
}

type PublishedState struct{}

func (s *PublishedState) OnEnter(context interface{}) error {
    fmt.Println("Document entered PUBLISHED state")
    return nil
}

func (s *PublishedState) OnExit(context interface{}) error {
    fmt.Println("Document exiting PUBLISHED state")
    return nil
}

func (s *PublishedState) GetName() string {
    return "PUBLISHED"
}

func ExampleStateMachine() {
    // Create state machine
    sm := NewStateMachine("DRAFT")
    
    // Add states
    sm.AddState(&DraftState{})
    sm.AddState(&ReviewState{})
    sm.AddState(&PublishedState{})
    
    // Define transitions
    sm.AddTransition("DRAFT", "submit_for_review", "REVIEW")
    sm.AddTransition("REVIEW", "approve", "PUBLISHED")
    sm.AddTransition("REVIEW", "reject", "DRAFT")
    sm.AddTransition("PUBLISHED", "unpublish", "DRAFT")
    
    // Test the state machine
    fmt.Printf("Current state: %s\n", sm.GetCurrentState())
    fmt.Printf("Valid actions: %v\n", sm.GetValidActions())
    
    // Submit for review
    err := sm.Trigger("submit_for_review", nil)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    }
    fmt.Printf("Current state: %s\n", sm.GetCurrentState())
    
    // Approve
    err = sm.Trigger("approve", nil)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    }
    fmt.Printf("Current state: %s\n", sm.GetCurrentState())
    
    // Try invalid action
    err = sm.Trigger("invalid_action", nil)
    if err != nil {
        fmt.Printf("Expected error: %v\n", err)
    }
}

Real-world Use Cases

Here’s where I commonly use the State pattern in Go:

  1. Order Processing: E-commerce order workflows with complex state transitions
  2. Document Workflows: Draft, review, approval, and publishing processes
  3. Game Development: Character states, game states, AI behavior states
  4. Network Connections: Connection states (connecting, connected, disconnected, error)
  5. User Authentication: Login states, session management, permission levels

Benefits of State Pattern

  1. Clean Code: Eliminates complex conditional statements
  2. Single Responsibility: Each state class has a single responsibility
  3. Easy Extension: Adding new states doesn’t require modifying existing code
  4. State Encapsulation: State-specific behavior is encapsulated in state classes
  5. Explicit State Transitions: Makes state transitions explicit and traceable

Caveats

While the State pattern is powerful, consider these limitations:

  1. Complexity: Can be overkill for simple state machines
  2. Class Proliferation: Creates many small state classes
  3. Memory Usage: Each state object consumes memory
  4. State Sharing: Sharing state objects between contexts can be tricky
  5. Debugging: State transitions can be harder to debug across multiple objects

Thank you

Thank you for reading! The State pattern is excellent for managing complex state-dependent behavior in Go applications. It provides a clean, maintainable way to handle state transitions and ensures that your objects behave correctly in each state. Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!