I. State — Behavioral Pattern
State lets an object change its behavior when its internal state changes, as if the object had switched to a different class. Instead of one big method full of if status == "pending" / else if status == "brewing" branches, each state is extracted into its own type that implements a shared interface, and the object simply delegates to whichever state it currently holds.
The problem this solves is state-conditional logic that grows unbounded. As more states and more transitions are added, a single method with branching logic becomes harder to read and easier to break — forgetting to update one branch when a new state is introduced is a common source of bugs. With State, adding a new state means adding a new type, not touching the existing ones.
It also makes illegal transitions explicit: a state simply doesn't provide a transition to a state it shouldn't reach.
II. Real-world Example
A coffee order that moves through a fixed lifecycle:
- Order (Context) — holds a reference to its current
OrderStateand delegatesAdvance()andStatus()to it. It doesn't contain any state-specific logic itself.
- OrderState (interface) — declares
Next(order *Order) stringandStatus() string, the contract every state must implement.
- PendingState — the order's starting state; advancing moves it to Brewing.
- BrewingState — the drink is being made; advancing moves it to Served.
- ServedState — the terminal state; advancing does nothing further, the order is done.
Each state decides for itself what the next state is — Order never needs to know the full lifecycle graph.

III. Implementation
order_state.go
package state
type OrderState interface {
Next(order *Order) string
Status() string
}order.go
package state
type Order struct {
ID string
Drink string
state OrderState
}
func NewOrder(id string, drink string) *Order {
order := &Order{
ID: id,
Drink: drink,
}
order.state = &PendingState{order: order}
return order
}
func (o *Order) Advance() string {
return o.state.Next(o)
}
func (o *Order) Status() string {
return o.state.Status()
}
func (o *Order) setState(state OrderState) {
o.state = state
}order_states.go
package state
type PendingState struct {
order *Order
}
func (s *PendingState) Next(order *Order) string {
order.setState(&BrewingState{order: order})
return "Order accepted, brewing started"
}
func (s *PendingState) Status() string {
return "Pending"
}
type BrewingState struct {
order *Order
}
func (s *BrewingState) Next(order *Order) string {
order.setState(&ServedState{order: order})
return "Drink is ready"
}
func (s *BrewingState) Status() string {
return "Brewing"
}
type ServedState struct {
order *Order
}
func (s *ServedState) Next(order *Order) string {
return "Order already served"
}
func (s *ServedState) Status() string {
return "Served"
}main.go (usage)
order := state.NewOrder("ORD-002", "Cappuccino")
fmt.Println("Status:", order.Status())
fmt.Println(order.Advance())
fmt.Println("Status:", order.Status())
fmt.Println(order.Advance())
fmt.Println("Status:", order.Status())IV. Explain the example above
state.NewOrder("ORD-002", "Cappuccino")creates an Order and sets its initial state to&PendingState{order: order}.
order.Status()delegates to the current state'sStatus()— at this point,PendingState.Status()returns"Pending".
order.Advance()callsPendingState.Next(order), which callsorder.setState(&BrewingState{order: order})to swap the internal state pointer, then returns a message describing the transition.
order.Status()now delegates toBrewingState.Status(), returning"Brewing".
order.Advance()again calls the current state'sNext— nowBrewingState.Next— which swaps toServedStateand returns its own message.
order.Status()finally returns"Served".
Running it produces:
*** Example State ***
Status: Pending
Order accepted, brewing started
Status: Brewing
Drink is ready
Status: Served
*** End of State ***Notice that Order.Advance() never changes — it always just calls o.state.Next(o). All the branching lives inside the state types themselves, and ServedState.Next is a natural place to guard against advancing past the terminal state.
V. Conclusion
State is worth reaching for when an object's behavior depends heavily on which "mode" it's in, and that set of modes is stable enough to model as types — order lifecycles, connection states, game character states, and similar finite-state workflows are a good fit. It trades a single method with visible branching for several small types, which makes each transition easy to reason about in isolation but harder to see the whole state machine at a glance without reading every state type.
For two or three states that rarely change, a plain enum with a switch statement is often simpler and easier to grep than a full State pattern implementation. The pattern earns its keep once the number of states or transitions grows enough that the switch statement itself becomes the maintenance burden.
No pattern is universally superior.
VI. References
- Go Design Patterns (Mario Castro Contreras)




