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:
- Order Processing: E-commerce order workflows with complex state transitions
- Document Workflows: Draft, review, approval, and publishing processes
- Game Development: Character states, game states, AI behavior states
- Network Connections: Connection states (connecting, connected, disconnected, error)
- User Authentication: Login states, session management, permission levels
Benefits of State Pattern
- Clean Code: Eliminates complex conditional statements
- Single Responsibility: Each state class has a single responsibility
- Easy Extension: Adding new states doesn’t require modifying existing code
- State Encapsulation: State-specific behavior is encapsulated in state classes
- Explicit State Transitions: Makes state transitions explicit and traceable
Caveats
While the State pattern is powerful, consider these limitations:
- Complexity: Can be overkill for simple state machines
- Class Proliferation: Creates many small state classes
- Memory Usage: Each state object consumes memory
- State Sharing: Sharing state objects between contexts can be tricky
- 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!