State Pattern #

An e-commerce order goes through many stages: created, awaiting payment, paid, being packed, shipped, arrived, and completed — or it can be cancelled any time before shipping, or returned after arrival. Each stage has different operations: an order awaiting payment can be paid or cancelled, but cannot be packed; a shipped order cannot be cancelled, but can be tracked. Without the State Pattern, OrderService fills up with conditions like if order.Status == "pending_payment" { ... } else if order.Status == "paid" { ... } — and every operation has to check the status before doing anything. The State Pattern moves this logic into state objects — each state knows which operations are valid and how to transition to the next state — so OrderService does not need to know anything about transition rules.

What Is the State Pattern? #

The State Pattern is a behavioral design pattern that lets an object change its behavior when its internal state changes, as if the object changed its class at runtime. Instead of ever-growing if-else or switch conditionals, each state is represented as a separate object implementing the behavior for that state.

The fundamental difference between the State Pattern and a mere “status enum”:

  • Behavior changes with the state — not just the label
  • Transitions are controlled — each state determines which states it can move to; invalid transitions produce errors, not silent bugs
  • States know the context — a state can trigger a transition to another state through the Context, without the client needing to know the mechanism
stateDiagram-v2
    [*] --> PendingPayment : Order created

    PendingPayment --> Paid : Pay()
    PendingPayment --> Cancelled : Cancel()

    Paid --> Processing : StartProcessing()
    Paid --> Cancelled : Cancel()

    Processing --> Shipped : Ship()

    Shipped --> Delivered : ConfirmDelivery()
    Shipped --> ReturnRequested : RequestReturn()

    Delivered --> ReturnRequested : RequestReturn()
    ReturnRequested --> Returned : ProcessReturn()

    Cancelled --> [*]
    Returned --> [*]
    Delivered --> [*]

The Problem It Solves #

The State Pattern solves two problems that appear together: piled-up conditional logic and uncontrolled state transitions.

Problem 1: Runaway if-else #

// ANTI-PATTERN: all state logic scattered across one struct
func (o *Order) Pay(amount float64) error {
    switch o.Status {
    case "pending_payment":
        if amount < o.TotalAmount {
            return fmt.Errorf("insufficient payment")
        }
        o.Status = "paid"
        o.PaidAt = time.Now()
        return nil
    case "paid":
        return fmt.Errorf("order already paid")
    case "cancelled":
        return fmt.Errorf("cannot pay cancelled order")
    default:
        return fmt.Errorf("cannot pay order in status: %s", o.Status)
    }
}

func (o *Order) Cancel() error {
    switch o.Status {
    case "pending_payment", "paid":
        o.Status = "cancelled"
        o.CancelledAt = time.Now()
        return nil
    case "shipped":
        return fmt.Errorf("cannot cancel shipped order")
    // ... every operation has its own switch that must always stay consistent
    }
}

// Problem: adding a new state means updating ALL switches in ALL methods
// If one switch is forgotten → a bug that is very hard to detect

// CORRECT: each state knows its own rules
func (o *Order) Pay(amount float64) error {
    return o.currentState.Pay(o, amount) // delegate to the active state
}

Problem 2: Uncontrolled Transitions #

// ANTI-PATTERN: anyone can set the status to anything
order.Status = "shipped" // can be done from anywhere, bypassing validation

// CORRECT: transitions only happen through controlled methods
order.Ship() // the state validates whether this transition is valid from the current state

Three Components of the State Pattern #

classDiagram
    class OrderState {
        <<interface>>
        +Pay(ctx *Order, amount float64) error
        +Cancel(ctx *Order) error
        +StartProcessing(ctx *Order) error
        +Ship(ctx *Order) error
        +ConfirmDelivery(ctx *Order) error
        +RequestReturn(ctx *Order) error
        +StateName() string
    }

    class Order {
        -state OrderState
        -id string
        -totalAmount float64
        -history []StateTransition
        +Pay(amount float64) error
        +Cancel() error
        +Ship() error
        +CurrentState() string
        +setState(state OrderState)
    }

    class PendingPaymentState {
        +Pay(ctx *Order, amount float64) error
        +Cancel(ctx *Order) error
        +StateName() string
    }

    class PaidState {
        +StartProcessing(ctx *Order) error
        +Cancel(ctx *Order) error
        +StateName() string
    }

    class ShippedState {
        +ConfirmDelivery(ctx *Order) error
        +RequestReturn(ctx *Order) error
        +StateName() string
    }

    OrderState <|.. PendingPaymentState
    OrderState <|.. PaidState
    OrderState <|.. ShippedState
    Order o-- OrderState : currentState
ComponentRoleIn Go
State interfaceDefines every operation that can happen in every stateAn interface with all possible methods
Concrete StateImplements behavior for a specific state; rejects invalid operationsA struct implementing the interface
ContextHolds a reference to the active state; delegates operations to itAn Order struct with a currentState field

Full Implementation: Order Lifecycle #

State Interface and Context #

package order

import (
    "fmt"
    "time"
)

// StateTransition records every state change for auditing.
type StateTransition struct {
    From      string
    To        string
    At        time.Time
    Reason    string
}

// OrderState defines every operation that can be performed on an order.
// Each state implements this interface; invalid operations
// return an error with an informative message.
type OrderState interface {
    Pay(ctx *Order, amount float64) error
    Cancel(ctx *Order, reason string) error
    StartProcessing(ctx *Order) error
    Ship(ctx *Order, trackingNumber, courier string) error
    ConfirmDelivery(ctx *Order) error
    RequestReturn(ctx *Order, reason string) error
    ProcessReturn(ctx *Order) error
    StateName() string
}

// Order is the Context — it holds the active state and delegates every operation.
type Order struct {
    ID            string
    UserID        string
    TotalAmount   float64
    PaidAmount    float64
    Items         []OrderItem
    TrackingNumber string
    Courier       string

    state         OrderState
    history       []StateTransition

    // Timestamps
    CreatedAt    time.Time
    PaidAt       *time.Time
    ShippedAt    *time.Time
    DeliveredAt  *time.Time
    CancelledAt  *time.Time
}

// OrderItem represents one item in an order.
type OrderItem struct {
    ProductID string
    Quantity  int
    Price     float64
}

// NewOrder creates a new order in the PendingPayment state.
func NewOrder(id, userID string, items []OrderItem, totalAmount float64) *Order {
    o := &Order{
        ID:          id,
        UserID:      userID,
        Items:       items,
        TotalAmount: totalAmount,
        CreatedAt:   time.Now(),
    }
    o.state = &PendingPaymentState{}
    return o
}

// setState swaps the active state and records the transition in the history.
// This method is called by concrete states, not by clients.
func (o *Order) setState(newState OrderState) {
    transition := StateTransition{
        From: o.state.StateName(),
        To:   newState.StateName(),
        At:   time.Now(),
    }
    o.history = append(o.history, transition)
    o.state = newState
}

// CurrentState returns the active state's name.
func (o *Order) CurrentState() string { return o.state.StateName() }

// History returns the state transition history.
func (o *Order) History() []StateTransition { return o.history }

// --- Public methods that delegate to the active state --- //

func (o *Order) Pay(amount float64) error {
    return o.state.Pay(o, amount)
}

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

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

func (o *Order) Ship(trackingNumber, courier string) error {
    return o.state.Ship(o, trackingNumber, courier)
}

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

func (o *Order) RequestReturn(reason string) error {
    return o.state.RequestReturn(o, reason)
}

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

BaseState: Default “Invalid Operation” #

// BaseState provides the default implementation for all operations.
// Every operation returns an "invalid transition" error.
// Concrete states only need to override the methods valid for that state.
type BaseState struct{}

func (b *BaseState) Pay(ctx *Order, amount float64) error {
    return fmt.Errorf("cannot pay order in state: %s", ctx.CurrentState())
}

func (b *BaseState) Cancel(ctx *Order, reason string) error {
    return fmt.Errorf("cannot cancel order in state: %s", ctx.CurrentState())
}

func (b *BaseState) StartProcessing(ctx *Order) error {
    return fmt.Errorf("cannot start processing order in state: %s", ctx.CurrentState())
}

func (b *BaseState) Ship(ctx *Order, trackingNumber, courier string) error {
    return fmt.Errorf("cannot ship order in state: %s", ctx.CurrentState())
}

func (b *BaseState) ConfirmDelivery(ctx *Order) error {
    return fmt.Errorf("cannot confirm delivery in state: %s", ctx.CurrentState())
}

func (b *BaseState) RequestReturn(ctx *Order, reason string) error {
    return fmt.Errorf("cannot request return in state: %s", ctx.CurrentState())
}

func (b *BaseState) ProcessReturn(ctx *Order) error {
    return fmt.Errorf("cannot process return in state: %s", ctx.CurrentState())
}

Concrete States #

// PendingPaymentState — awaiting payment.
// Valid operations: Pay, Cancel.
type PendingPaymentState struct{ BaseState }

func (s *PendingPaymentState) Pay(ctx *Order, amount float64) error {
    if amount < ctx.TotalAmount {
        return fmt.Errorf("payment insufficient: need Rp %.0f, got Rp %.0f",
            ctx.TotalAmount, amount)
    }

    now := time.Now()
    ctx.PaidAmount = amount
    ctx.PaidAt = &now
    ctx.setState(&PaidState{})
    return nil
}

func (s *PendingPaymentState) Cancel(ctx *Order, reason string) error {
    now := time.Now()
    ctx.CancelledAt = &now
    ctx.setState(&CancelledState{Reason: reason})
    return nil
}

func (s *PendingPaymentState) StateName() string { return "pending_payment" }


// PaidState — payment confirmed, waiting to be processed.
// Valid operations: StartProcessing, Cancel.
type PaidState struct{ BaseState }

func (s *PaidState) StartProcessing(ctx *Order) error {
    ctx.setState(&ProcessingState{})
    return nil
}

func (s *PaidState) Cancel(ctx *Order, reason string) error {
    // Payment has already come in — a refund is needed
    now := time.Now()
    ctx.CancelledAt = &now
    ctx.setState(&CancelledState{Reason: reason, NeedsRefund: true})
    return nil
}

func (s *PaidState) StateName() string { return "paid" }


// ProcessingState — being packed/processed.
// Valid operations: Ship.
type ProcessingState struct{ BaseState }

func (s *ProcessingState) Ship(ctx *Order, trackingNumber, courier string) error {
    if trackingNumber == "" {
        return fmt.Errorf("tracking number is required to ship order")
    }
    if courier == "" {
        return fmt.Errorf("courier name is required to ship order")
    }

    now := time.Now()
    ctx.TrackingNumber = trackingNumber
    ctx.Courier = courier
    ctx.ShippedAt = &now
    ctx.setState(&ShippedState{})
    return nil
}

func (s *ProcessingState) StateName() string { return "processing" }


// ShippedState — shipped, in transit.
// Valid operations: ConfirmDelivery, RequestReturn.
type ShippedState struct{ BaseState }

func (s *ShippedState) ConfirmDelivery(ctx *Order) error {
    now := time.Now()
    ctx.DeliveredAt = &now
    ctx.setState(&DeliveredState{})
    return nil
}

func (s *ShippedState) RequestReturn(ctx *Order, reason string) error {
    ctx.setState(&ReturnRequestedState{Reason: reason})
    return nil
}

func (s *ShippedState) StateName() string { return "shipped" }


// DeliveredState — received by the buyer.
// Valid operations: RequestReturn (within a certain time window).
type DeliveredState struct{ BaseState }

func (s *DeliveredState) RequestReturn(ctx *Order, reason string) error {
    if ctx.DeliveredAt == nil {
        return fmt.Errorf("delivery timestamp not set")
    }
    // Returns are only allowed within 7 days after delivery
    if time.Since(*ctx.DeliveredAt) > 7*24*time.Hour {
        return fmt.Errorf("return window has expired (7 days after delivery)")
    }
    ctx.setState(&ReturnRequestedState{Reason: reason})
    return nil
}

func (s *DeliveredState) StateName() string { return "delivered" }


// ReturnRequestedState — return requested, waiting to be processed.
// Valid operations: ProcessReturn.
type ReturnRequestedState struct {
    BaseState
    Reason string
}

func (s *ReturnRequestedState) ProcessReturn(ctx *Order) error {
    ctx.setState(&ReturnedState{})
    return nil
}

func (s *ReturnRequestedState) StateName() string { return "return_requested" }


// ReturnedState — the goods have been returned. Terminal state.
type ReturnedState struct{ BaseState }

func (s *ReturnedState) StateName() string { return "returned" }


// CancelledState — order cancelled. Terminal state.
type CancelledState struct {
    BaseState
    Reason      string
    NeedsRefund bool
}

func (s *CancelledState) StateName() string { return "cancelled" }

Demonstration: An Order Going Through Its Full Lifecycle #

func main() {
    items := []order.OrderItem{
        {ProductID: "PRD-001", Quantity: 2, Price: 150000},
        {ProductID: "PRD-002", Quantity: 1, Price: 75000},
    }

    o := order.NewOrder("ORD-001", "user-123", items, 375000)
    fmt.Printf("Created: %s\n", o.CurrentState()) // pending_payment

    // Happy path: pay → process → ship → receive
    if err := o.Pay(375000); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("After pay: %s\n", o.CurrentState()) // paid

    if err := o.StartProcessing(); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("After processing: %s\n", o.CurrentState()) // processing

    if err := o.Ship("JNE1234567890", "JNE"); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("After ship: %s (tracking: %s)\n", o.CurrentState(), o.TrackingNumber)

    if err := o.ConfirmDelivery(); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("After delivery: %s\n", o.CurrentState()) // delivered

    // Try an invalid operation — the state rejects it with a clear message
    if err := o.Cancel("changed my mind"); err != nil {
        fmt.Printf("Cannot cancel: %v\n", err)
        // "cannot cancel order in state: delivered"
    }

    // Show the transition history
    fmt.Println("\nOrder History:")
    for _, t := range o.History() {
        fmt.Printf("  %s → %s (%s)\n", t.From, t.To, t.At.Format("15:04:05"))
    }
}

Guard Conditions: Validation Before Transition #

A guard condition is an additional check that must be satisfied before a state transition is allowed — beyond the current-state check.

// PendingPaymentState with more complete guard conditions
func (s *PendingPaymentState) Pay(ctx *Order, amount float64) error {
    // Guard 1: payment amount
    if amount <= 0 {
        return fmt.Errorf("payment amount must be positive")
    }
    if amount < ctx.TotalAmount {
        return fmt.Errorf("insufficient payment: expected Rp %.0f, got Rp %.0f",
            ctx.TotalAmount, amount)
    }

    // Guard 2: the order is still within the payment window (24 hours)
    paymentDeadline := ctx.CreatedAt.Add(24 * time.Hour)
    if time.Now().After(paymentDeadline) {
        // Auto-cancel and transition to the cancelled state
        ctx.setState(&CancelledState{Reason: "payment timeout", NeedsRefund: false})
        return fmt.Errorf("payment deadline exceeded: order automatically cancelled")
    }

    // Guard 3: items are still available (in a real implementation: check inventory)
    // if !inventoryAvailable(ctx.Items) {
    //     return fmt.Errorf("some items are no longer available")
    // }

    now := time.Now()
    ctx.PaidAmount = amount
    ctx.PaidAt = &now
    ctx.setState(&PaidState{})
    return nil
}

History States: Tracking Transition History #

Adding metadata to every transition makes the system more auditable and debuggable.

// A richer StateTransition with contextual information
type StateTransition struct {
    From      string
    To        string
    At        time.Time
    Actor     string // who triggered the transition (user ID, system)
    Reason    string
    Metadata  map[string]interface{}
}

// A more informative setState
func (o *Order) setStateWithAudit(newState OrderState, actor, reason string, meta map[string]interface{}) {
    transition := StateTransition{
        From:     o.state.StateName(),
        To:       newState.StateName(),
        At:       time.Now(),
        Actor:    actor,
        Reason:   reason,
        Metadata: meta,
    }
    o.history = append(o.history, transition)
    o.state = newState
}

// Usage in a concrete state:
func (s *PendingPaymentState) Pay(ctx *Order, amount float64) error {
    // ... validation ...
    ctx.setStateWithAudit(&PaidState{}, "payment-gateway", "payment confirmed",
        map[string]interface{}{
            "amount":         amount,
            "payment_method": "credit_card",
        },
    )
    return nil
}

State Pattern vs Strategy Pattern #

State and Strategy are two patterns often confused because both use composition to change behavior. The critical difference is in who controls the swap and whether there are relationships between the “states”.

// Strategy: the client chooses the algorithm from outside
svc.SetShippingStrategy(jneStrategy)
svc.SetShippingStrategy(jtStrategy)
// The client can set any algorithm any time; no order is enforced

// State: the object transitions itself based on internal conditions
order.Pay(amount)    // PendingPayment → Paid (the state determines the transition)
order.Ship(tracking) // must go through Processing first; cannot skip
// The client cannot force an invalid transition
AspectStateStrategy
Who swapsThe object itself (via setState from inside a State)The client from outside
Do states know each other?Yes — state A knows state B for transitionsNo — every strategy is independent
Is an order/flow enforced?Yes — only valid transitions are allowedNo — can switch to any strategy
ExampleOrder lifecycle, network connectionSort algorithms, shipping methods

Second Case Study: Network Connection State #

// ConnectionState defines every operation on a network connection.
type ConnectionState interface {
    Connect(ctx *Connection) error
    Disconnect(ctx *Connection) error
    Send(ctx *Connection, data []byte) error
    Receive(ctx *Connection) ([]byte, error)
    StateName() string
}

type Connection struct {
    host    string
    port    int
    state   ConnectionState
    conn    net.Conn
}

func NewConnection(host string, port int) *Connection {
    c := &Connection{host: host, port: port}
    c.state = &DisconnectedState{}
    return c
}

// DisconnectedState — not connected yet.
// Valid operations: Connect.
type DisconnectedState struct{}

func (s *DisconnectedState) Connect(ctx *Connection) error {
    addr := fmt.Sprintf("%s:%d", ctx.host, ctx.port)
    conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
    if err != nil {
        return fmt.Errorf("connection failed: %w", err)
    }
    ctx.conn = conn
    ctx.state = &ConnectedState{}
    return nil
}

func (s *DisconnectedState) Disconnect(ctx *Connection) error {
    return fmt.Errorf("already disconnected")
}

func (s *DisconnectedState) Send(ctx *Connection, data []byte) error {
    return fmt.Errorf("cannot send: not connected")
}

func (s *DisconnectedState) Receive(ctx *Connection) ([]byte, error) {
    return nil, fmt.Errorf("cannot receive: not connected")
}

func (s *DisconnectedState) StateName() string { return "disconnected" }


// ConnectedState — connected and ready.
// Valid operations: Send, Receive, Disconnect.
type ConnectedState struct{}

func (s *ConnectedState) Connect(ctx *Connection) error {
    return fmt.Errorf("already connected")
}

func (s *ConnectedState) Disconnect(ctx *Connection) error {
    if err := ctx.conn.Close(); err != nil {
        return fmt.Errorf("disconnect failed: %w", err)
    }
    ctx.conn = nil
    ctx.state = &DisconnectedState{}
    return nil
}

func (s *ConnectedState) Send(ctx *Connection, data []byte) error {
    _, err := ctx.conn.Write(data)
    if err != nil {
        ctx.state = &DisconnectedState{} // auto-transition on error
        return fmt.Errorf("send failed (connection dropped): %w", err)
    }
    return nil
}

func (s *ConnectedState) Receive(ctx *Connection) ([]byte, error) {
    buf := make([]byte, 4096)
    n, err := ctx.conn.Read(buf)
    if err != nil {
        ctx.state = &DisconnectedState{}
        return nil, fmt.Errorf("receive failed (connection dropped): %w", err)
    }
    return buf[:n], nil
}

func (s *ConnectedState) StateName() string { return "connected" }

Testing the State Pattern #

func TestOrder_HappyPath(t *testing.T) {
    o := NewTestOrder()

    if o.CurrentState() != "pending_payment" {
        t.Errorf("expected pending_payment, got %s", o.CurrentState())
    }

    if err := o.Pay(375000); err != nil {
        t.Fatalf("Pay failed: %v", err)
    }
    if o.CurrentState() != "paid" {
        t.Errorf("expected paid after Pay, got %s", o.CurrentState())
    }

    if err := o.StartProcessing(); err != nil {
        t.Fatalf("StartProcessing failed: %v", err)
    }

    if err := o.Ship("TRACK001", "JNE"); err != nil {
        t.Fatalf("Ship failed: %v", err)
    }

    if err := o.ConfirmDelivery(); err != nil {
        t.Fatalf("ConfirmDelivery failed: %v", err)
    }
    if o.CurrentState() != "delivered" {
        t.Errorf("expected delivered, got %s", o.CurrentState())
    }

    // Verify the history
    if len(o.History()) != 4 {
        t.Errorf("expected 4 transitions, got %d", len(o.History()))
    }
}

func TestOrder_InvalidTransitionReturnsError(t *testing.T) {
    o := NewTestOrder()

    // Cannot ship before paying
    err := o.Ship("TRACK001", "JNE")
    if err == nil {
        t.Error("expected error when shipping before payment")
    }

    // Cannot process before paying
    err = o.StartProcessing()
    if err == nil {
        t.Error("expected error when processing before payment")
    }

    // Pay first
    _ = o.Pay(375000)

    // Cannot pay again
    err = o.Pay(375000)
    if err == nil {
        t.Error("expected error when paying twice")
    }
}

func TestOrder_CancelAfterPaymentNeedsRefund(t *testing.T) {
    o := NewTestOrder()
    _ = o.Pay(375000)

    if err := o.Cancel("changed my mind"); err != nil {
        t.Fatalf("Cancel failed: %v", err)
    }
    if o.CurrentState() != "cancelled" {
        t.Errorf("expected cancelled, got %s", o.CurrentState())
    }
}

func TestOrder_ReturnWindowExpired(t *testing.T) {
    o := NewTestOrder()
    _ = o.Pay(375000)
    _ = o.StartProcessing()
    _ = o.Ship("TRACK001", "JNE")
    _ = o.ConfirmDelivery()

    // Manipulate the delivery time to 8 days ago
    eightDaysAgo := time.Now().Add(-8 * 24 * time.Hour)
    o.DeliveredAt = &eightDaysAgo

    err := o.RequestReturn("defective product")
    if err == nil {
        t.Error("expected error when return window expired")
    }
}

func TestPendingPaymentState_InsufficientPayment(t *testing.T) {
    o := NewTestOrder()
    err := o.Pay(100) // far below the total amount
    if err == nil {
        t.Error("expected error for insufficient payment")
    }
    if o.CurrentState() != "pending_payment" {
        t.Errorf("state should not change after failed payment")
    }
}

func NewTestOrder() *Order {
    return NewOrder("TEST-001", "user-1", []OrderItem{
        {ProductID: "PRD-1", Quantity: 1, Price: 375000},
    }, 375000)
}

When to Use and When Not to #

USE the State Pattern if:
  ✓ An object's behavior differs significantly based on its state
  ✓ There are transition rules that must be enforced (not every transition is valid)
  ✓ Status-based if-else or switch has grown too large and duplicated
  ✓ New states will be added often without changing the Context or other states
  ✓ You need a clear audit trail of when and why the state changed

AVOID the State Pattern if:
  ✗ There are only 2-3 simple states that will not grow
  ✗ Behaviors between states are not different enough to separate
  ✗ Every state is valid from every other state (no transition rules)
  ✗ States do not need to transition themselves — use a simple enum

State Machine Library vs State Pattern

For very complex state machines with many states and transitions, consider using a state machine library like looplab/fsm, which provides declarative state and transition definitions. The State Pattern is better suited when each state has complex, distinct behavior — not just a different label. Use a library when you need features like guards, callbacks, and state machine visualization.


State Pattern Review Checklist #

DESIGN:
  □ Each state only implements the methods valid for that state
  □ Invalid methods return an informative error (not a panic)
  □ BaseState avoids duplicating "invalid operation" implementations
  □ Transitions always go through setState, never direct assignment

TRANSITIONS:
  □ States that create transitions are verified correct (states cannot be skipped)
  □ Guard conditions are validated before setState is called
  □ Terminal states (Cancelled, Returned, Delivered) cannot transition
  □ Transition history is recorded with timestamps and sufficient information

TESTING:
  □ Happy path: every valid transition is tested in sequence
  □ Every invalid transition is tested — the correct error is returned
  □ The state does not change after a failed operation
  □ Guard conditions are tested (insufficient payment, expired window, etc.)
  □ Terminal states cannot transition further

Summary #

  • The State Pattern delegates behavior to state objects — the Context (Order) knows no transition rules; each state decides what is valid and which state to transition to.
  • Invalid transitions produce errors, not silent bugs — this is the State Pattern’s main guarantee; there is no way to force an unauthorized transition from outside.
  • BaseState eliminates duplication — implement the default “invalid operation” once in BaseState; concrete states only override the methods valid for that state.
  • setState is only called from inside a State, never from a client — this is what preserves encapsulation; the Context provides setState as a package-level method, not a public API.
  • Guard conditions strengthen transitions — additional validation (payment deadline, return window) is implemented inside the state before calling setState.
  • Transition history is very valuable — record who triggered it, when, and why; this greatly helps debugging and auditing.
  • Distinguish it from Strategy: State transitions itself and states know each other; Strategy is chosen from outside and every strategy is independent.
  • For complex state machines, consider a library like looplab/fsm — the State Pattern is better when per-state behavior is very different and rich.

← Previous: Chain of Responsibility   Next: Template Method →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact