State is a behavioral design pattern that allows an object to change its behavior when its internal state changes. The State Pattern is a behavioral design pattern that enables an object to change its behavior in response to changes in its internal state. It’s beneficial when an object must behave differently depending on its current state, but without resorting to large conditional statements (like if-else or switch chains).
I’d like to extend heartfelt thanks to Mohammad Reza Kargar for not only grasping the essence of the State design pattern in Golang but also skillfully implementing it to address complex conditional behaviors.
The State pattern is closely related to the concept of a Finite-State Machine.

Why Use a State Machine?
State machines help enforce predictable transitions between states. They are useful in:
- Order processing systems
- Workflow engines
- Vending machines
- Booking applications
🥤 Example: Vending Machine
Imagine a simple vending machine that sells only one kind of snack (e.g., a bag of chips). The machine can be in one of four states:
✅ 1. hasItem
- The machine has chips in stock.
- You can request to buy an item.
Example:
You walk up to the machine and see that it has chips inside. You can press a button to make a purchase.
❌ 2. noItem
- The machine is empty.
- It won’t let you buy anything.
Example:
You press the button, but it shows “Out of stock.” No chips are available.
🔘 3. itemRequested
- You have requested to purchase an item, but you have not yet paid.
- The machine is waiting for your money.
Example:
You press the button for chips. The machine now shows “Please insert $1.”
💰 4. hasMoney
- You’ve inserted the money, and the machine is ready to give you the item.
Example:
You put in $1 after requesting chips. The machine gives you the chips and returns to the correct state depending on whether it still has more items.
The main idea is that, at any given moment, there’s a finite number of states that a program can be in. Within any unique state, the program behaves differently, and it can be switched from one state to another instantaneously. However, depending on the current state, the program may or may not switch to certain other states. These switching rules, called transitions, are also finite and predetermined.
You can also apply this approach to objects. Imagine that we have a Document class. A document can be in one of three states: Draft, Moderation and Published. The publish The method of the document works a little bit differently in each state:
- In
DraftIt moves the document to moderation. - In
ModerationIt makes the document public, but only if the current user is an administrator. - In
PublishedIt doesn’t do anything at all.

State Machine Using Conditionals
- A state machine is a way of organizing code where something (like a document) can be in different states (like “Draft”, “Published”, or “Archived”).
- Using conditionals means you write a lot of
iforswitchstatements to decide what to do based on the current state.
Why Conditionals Can Be a Problem
- As you add more states and more behaviors that depend on the state, you end up writing a lot of
iforswitchstatements in many different places. - This can make your code hard to read, hard to change, and easy to break.
🧱 Example:
Imagine you’re coding a Document class in a writing app.
Problem: As you add more states (like “review”, “scheduled”) and actions (Edit(), Delete(), etc.), every method becomes a long mess of if-else logic.
type Document struct {
State string // could be "draft", "published", "archived"
}
func (d *Document) Publish() {
if d.State == "draft" {
fmt.Println("Document is now published.") d.State = "published"
}
else if d.State == "published" {
fmt.Println("Document is already published.")
}
else if d.State == "archived" {
fmt.Println("Cannot publish an archived document.")
}
}🧠 What Is the State Pattern?
Imagine a vending machine. It behaves differently depending on what state it’s in:
- If it’s empty, → You can’t buy anything.
- If it has items → You can request an item.
- If you’ve selected an item → It waits for money.
- If you insert enough money, → It dispenses the item.
Instead of writing a bunch of if and switch statements, the State pattern gives each of these conditions its object, and the machine delegates its behavior to the current state.
🏗️ Code Breakdown
1. State Interface (state.go)
type State interface {
addItem(int) error
requestItem() error
insertMoney(money int) error
dispenseItem() error
}2. VendingMachine Struct (vendingMachine.go)
This is the actual vending machine. It holds:
currentState: the current state object (e.g., waiting for money)- States:
hasItem,itemRequested,hasMoney,noItem itemCountanditemPrice
It delegates behavior to its current state like this:
func (v *VendingMachine) requestItem() error {
return v.currentState.requestItem()
}3. Concrete State Implementations
✅ HasItemState – The machine has stock and is waiting for you
func (i *HasItemState) requestItem() error {
i.vendingMachine.setState(i.vendingMachine.itemRequested)
}💵 ItemRequestedState – Item selected, waiting for money
func (i *ItemRequestedState) insertMoney(money int) error {
if money < price {
return error
}
i.vendingMachine.setState(i.vendingMachine.hasMoney)
}🍬 HasMoneyState – Ready to dispense
func (i *HasMoneyState) dispenseItem() error {
vendingMachine.itemCount -= 1
vendingMachine.setState(...)
}❌ NoItemState – The machine is empty
func (i *NoItemState) requestItem() error {
return fmt.Errorf("Item out of stock")
}4. Client Code (main.go)
vendingMachine := newVendingMachine(1,
10)vendingMachine.requestItem()vendingMachine.insertMoney(10)vendingMachine.dispenseItem()✅ Sample Run Output
Item requestdMoney entered is okDispensing ItemAdding 2 itemsItem requestdMoney entered is
okDispensing ItemImplement the machine state
Below is a compact, self‑contained Go example that applies the State pattern to an order/payment workflow with the two operations you specified — approve() and refund()—and the five states you listed:
- unknown (brand‑new order)
- checkout (details entered, awaiting approval to start)
- In progress (payment/fulfilment running)
- succeed (completed successfully)
- canceled (refunded / void)
The Order (the context) delegates every call to its currentState, and each concrete state decides
• whether the action is allowed,
• what side effects to perform, and
• Which next state to switch to?
package main
import (
"fmt"
"log")/* ---------- State interface ---------- */type State interface {
approve() error refund() error
}
/* ---------- Context ---------- */type Order struct {
// singleton state objects unknown
State checkout State inProgress State succeed
State canceled State currentState State id string
}
func newOrder(id string) *Order {
o := &Order{
id: id
}
// create concrete states, each holds a back‑pointer to the context o.unknown = &unknownState{
o
}
o.checkout = &checkoutState{
o
}
o.inProgress = &inProgressState{
o
}
o.succeed = &succeedState{
o
}
o.canceled = &canceledState{
o
}
o.setState(o.unknown) // initial state
return o
}
/* -- context helpers that delegate to state -- */func (o *Order) approve() error {
return o.currentState.approve()
}
func (o *Order) refund() error {
return o.currentState.refund()
}
func (o *Order) setState(s State) {
o.currentState = s fmt.Printf("🟢 Order %s switched to %T\n", o.id, s)
}
/* ---------- Concrete states ---------- *//* 1.unknown
-------------------------------------------------------------- */
type unknownState struct{
order *Order
}
func (s *unknownState) approve() error {
fmt.Println("details confirmed, moving to checkout") s.order.setState(s.order.checkout) return
nil
}
func (s *unknownState) refund() error {
return fmt.Errorf("cannot refund: order not created yet")
}
/* 2.checkout ------------------------------------------------------------- */type checkoutState
struct{
order *Order
}
func (s *checkoutState) approve() error {
fmt.Println("payment started") s.order.setState(s.order.inProgress) return nil
}
func (s *checkoutState) refund() error {
s.order.setState(s.order.canceled) return nil
}
/* 3.inProgress ----------------------------------------------------------- */type inProgressState
struct{
order *Order
}
func (s *inProgressState) approve() error {
fmt.Println("order fulfilled") s.order.setState(s.order.succeed) return nil
}
func (s *inProgressState) refund() error {
fmt.Println("cancelling in‑progress order") s.order.setState(s.order.canceled) return nil
}
/* 4.succeed -------------------------------------------------------------- */type succeedState
struct{
order *Order
}
func (s *succeedState) approve() error {
return fmt.Errorf("order already succeeded")
}
func (s *succeedState) refund() error {
fmt.Println("refund processed") s.order.setState(s.order.canceled) return nil
}
/* 5.canceled ------------------------------------------------------------- */type canceledState
struct{
order *Order
}
func (s *canceledState) approve() error {
return fmt.Errorf("cannot approve: order is canceled")
}
func (s *canceledState) refund() error {
return fmt.Errorf("order already canceled / refunded")
}
/* ---------- Demo ---------- */func main() {
o := newOrder("ORD‑42") // happy path if err := o.approve();
err != nil {
log.Fatal(err)
}
// unknown -> checkout if err := o.approve();
err != nil {
log.Fatal(err)
}
// checkout -> inProgress if err := o.approve();
err != nil {
log.Fatal(err)
}
// inProgress -> succeed // now refund it if err := o.refund();
err != nil {
log.Fatal(err)
}
// succeed -> canceled // try an invalid transition if err := o.approve();
err != nil {
fmt.Println("expected error:", err)
}
}
Building a State Machine in Go
State machines are a powerful design pattern for modeling complex workflows and systems with well-defined states and transitions. In this article, we’ll implement a Finite State Machine (FSM) in Go using a clean and scalable design.
✅ FSM Design in Go
We’ll create:
- States: Defined as constants.
- Actions: Events that trigger transitions.
- Transition Table: Maps
From State + ActiontoTo State. - Execution Logic: Optionally execute business logic during transitions.
1. Define States and Actions
type State stringtype Action stringconst ( StateIdle State = "idle" StateProcessing State
= "processing" StateSuccess State = "success" StateFailed State = "failed"
ActionStart Action = "start" ActionSucceed Action = "succeed" ActionFail Action = "fail")2. Define Transition Struct
type Transition struct {
From State
Act Action
To
State
Func func() error // Optional business logic
}3. FSM Core Logic
type FSM struct {
State State
Transitions []Transition
}
func (f *FSM) Can(action Action) bool {
for _, t := range f.Transitions {
if t.From == f.State && t.Act == action {
return true
}
}
return false
}
func (f *FSM) Apply(action Action) error {
for _, t := range f.Transitions {
if t.From == f.State && t.Act == action {
if t.Func != nil {
if err := t.Func();
err != nil {
return err
}
}
f.State = t.To return nil
}
}
return fmt.Errorf("invalid transition from %s with action %s", f.State, action)
}4. Define Transitions Table
func main() {
fsm := &FSM{
State: StateIdle, Transitions: []Transition{
{
From: StateIdle, Act: ActionStart, To: StateProcessing, Func: func() error {
fmt.Println("Started processing") return nil
}
}, {
From: StateProcessing, Act: ActionSucceed, To: StateSuccess, Func: func() error {
fmt.Println("Processing succeeded") return nil
}
}, {
From: StateProcessing, Act: ActionFail, To: StateFailed, Func: func() error {
fmt.Println("Processing failed") return nil
}
},
},
}
fmt.Println("Current State:", fsm.State)
fsm.Apply(ActionStart)
fmt.Println("Current State:", fsm.State)
fsm.Apply(ActionSucceed)
fmt.Println("Current State:", fsm.State)
}✅ Features of This Design
- Type Safety: Strong typing for states and actions.
- Pluggable Logic: Each transition can run custom business logic.
- Declarative: Easy to read and maintain.
✅ Possible Enhancements
- Add guards (conditional checks before transitions)
- Support multiple actions per transition
- Persist the FSM state to a database
- Use service injection for business logic
How does the State pattern in Go overcome the limitations of conditional statements?
The State design pattern in Go effectively overcomes the limitations of extensive conditional statements by encapsulating state-specific behavior into separate objects and allowing the main object to delegate actions to its current state object.
Here’s a breakdown of how it addresses these limitations:
Limitations of Conditional Statements
• Messy and Hard to Manage Code: When an object needs to behave differently based on its internal state, a common approach is to use numerous if-else or switch statements within its methods
• Increased Complexity: As more states and behaviors are added, these conditional chains become long, difficult to read, and hard to change. For instance, a Document A class with states like "Draft," "Published," and "Archived" would require if-else logic in every method (e.g., Publish(), Edit(), Delete()) to determine what action is valid for the current state.
• Violation of Open/Closed Principle: Modifying or adding a new state often requires changes across multiple methods, making the code easy to break and difficult to extend without altering existing, tested code.
• Duplication of Logic: Similar conditional logic might be scattered throughout the codebase.
How the State Pattern Overcomes These Limitations
The State pattern addresses these issues by:
1. Defining a State Interface: It starts by defining an interface (e.g., State in Go) that declares all possible behaviors that can vary by state. For a vending machine, this might include methods like addItem, requestItem, insertMoney, and dispenseItem. For an order system, it could be approve() and refund().
2. Encapsulating State-Specific Behavior: Instead of one large object with many conditionals, each specific state (e.g., HasItemState, NoItemState, ItemRequestedState, HasMoneyState for a vending machine; or unknownState, checkoutState, inProgressState, succeedState, canceledState for an order) becomes its separate class or struct that implements the State interface. Within each concrete state object, only the logic relevant to that specific state is implemented for the interface methods.
3. Delegation of Behavior: The main object (known as the “context,” such as VendingMachine or Order) maintains a reference to its current state object. When an action is called on the context object (e.g., vendingMachine.requestItem(), order.approve()), it delegates that call to its currentState object. The currentState then handles the action according to its specific implementation.
◦ Example (Vending Machine): If the vending machine is in HasItemState and requestItem() is called, that state object handles the request and transitions the machine to ItemRequestedState. If it’s in NoItemState and requestItem() is called, the NoItemState object returns an "Item out of stock" error.
◦ Example (Order System): An Order object delegates approve() or refund() calls to its currentState. For example, an unknownState can approve() to move to checkout, but a canceledState will return an error if approve() is called.
4. Predictable State Transitions: Each concrete state object is also responsible for determining what the next state should be after an action, enforcing predictable transitions. This is explicitly managed within the state implementations, such as s.order.setState(s.order.checkout).
5. Improved Readability and Maintainability: By isolating the behavior for each state into its own class, the code becomes much cleaner. There are no large if-else or switch statements in the main object’s methods. Adding new states or modifying existing state behavior only requires creating a new concrete state class or modifying an existing one, rather than changing multiple conditional blocks across the entire system.
6. Scalability: This design makes the system highly scalable. If new states or actions are required, you simply create new concrete state implementations without altering existing code, adhering to the Open/Closed Principle.
In essence, the State pattern transforms complex conditional logic into an object-oriented structure where each “if” condition becomes a separate object, making the system more modular, understandable, and easier to evolve. It is closely related to the concept of a Finite-State Machine (FSM), which inherently defines states, actions, and transitions
Conclution
Both the State design pattern and FSM implementations in Go provide powerful ways to manage complex state-dependent logic, offering significant advantages over verbose conditional statements. The State pattern emphasizes an object-oriented approach where behavior is delegated to state objects, while the transition table FSM offers a more declarative and tabular way to define state changes, especially beneficial for complex workflows. Choosing between them depends on the specific project needs and architectural preferences. Regardless of the chosen approach, state machines help enforce predictable transitions and lead to more maintainable and robust code.
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 200k supporters? We do not get paid by Medium!
If you want to show some love, please take a moment to follow me on LinkedIn, TikTok and Instagram. And before you go, don’t forget to clap and follow the writer️!
