The Problem
Objects with life cycles often accumulate large switch statements over status fields. An order may allow payment only in one state, shipping in another, and cancellation in several. As the workflow grows, those branches become harder to test and easier to break.
The Solution
State moves behavior and transition rules into dedicated state objects. In Go, the context stores an interface for the current state and delegates stateful operations to it. Each concrete state decides whether an action is allowed and what the next state should be.
Structure
- Context:
Orderstores the current state. - State interface: Declares the operations that vary by state.
- Concrete states: Pending, paid, shipped, and cancelled states enforce different rules.
- Client: Calls operations on the order without branching on status values.
Implementation
This example models a basic order workflow. Each state object owns the legal transitions, and the order delegates Pay, Ship, and Cancel to whichever state is active.
package main
type Order struct {
ID string
state OrderState
}
func NewOrder(id string) *Order {
return &Order{ID: id, state: PendingState{}}
}
func (o *Order) setState(state OrderState) {
o.state = state
}
func (o *Order) Status() string {
return o.state.Name()
}
func (o *Order) Pay() error {
return o.state.Pay(o)
}
func (o *Order) Ship() error {
return o.state.Ship(o)
}
func (o *Order) Cancel() error {
return o.state.Cancel(o)
} Best Practices
- Use State when behavior truly changes by lifecycle stage, not just because a label exists.
- Keep state interfaces focused on the operations that vary.
- Let concrete states own transitions so invalid moves stay localized.
- If state objects need no data, zero-sized structs keep the pattern lightweight in Go.
- Do not introduce State if a small switch is still simpler and unlikely to grow.
When to Use
- An object has a lifecycle with different allowed operations per stage.
- Status-based conditionals are spreading across the codebase.
- You want invalid transitions expressed close to the rules themselves.
When NOT to Use
- There are only a couple of stable branches and they are unlikely to grow.
- The transition logic is simpler as a small explicit switch.
- The extra objects would hide rather than clarify the workflow.