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| Component | Role | In Go |
|---|---|---|
| State interface | Defines every operation that can happen in every state | An interface with all possible methods |
| Concrete State | Implements behavior for a specific state; rejects invalid operations | A struct implementing the interface |
| Context | Holds a reference to the active state; delegates operations to it | An 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
| Aspect | State | Strategy |
|---|---|---|
| Who swaps | The object itself (via setState from inside a State) | The client from outside |
| Do states know each other? | Yes — state A knows state B for transitions | No — every strategy is independent |
| Is an order/flow enforced? | Yes — only valid transitions are allowed | No — can switch to any strategy |
| Example | Order lifecycle, network connection | Sort 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.