Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
โฑ๏ธ 9 min read

State Pattern

The State pattern lets an object change its behavior when its internal state changes. Instead of giant if (state == X) conditionals scattered throughout the class, each state becomes its own object with its own behavior. The context delegates to whichever state is currently active.

Why this matters: Vending machines, elevators, order systems, and game entities all have lifecycles with distinct states. Interviewers watch for whether you model state transitions cleanly or bury them in nested conditionals.


Prerequisites


Class Diagram

classDiagram
    class VendingMachine {
        -currentState: State
        +insertCoin(amount)
        +selectProduct(code)
        +dispense()
        +setState(State)
    }
    class State {
        <<interface>>
        +insertCoin(machine, amount)
        +selectProduct(machine, code)
        +dispense(machine)
    }
    class IdleState {
        +insertCoin(machine, amount)
        +selectProduct(machine, code)
        +dispense(machine)
    }
    class HasMoneyState {
        +insertCoin(machine, amount)
        +selectProduct(machine, code)
        +dispense(machine)
    }
    class DispensingState {
        +insertCoin(machine, amount)
        +selectProduct(machine, code)
        +dispense(machine)
    }

    VendingMachine --> State
    State <|.. IdleState
    State <|.. HasMoneyState
    State <|.. DispensingState

The Problem: Conditional Explosion

stateDiagram-v2
    [*] --> Idle
    Idle --> HasMoney: insertCoin
    HasMoney --> HasMoney: insertCoin - add more
    HasMoney --> Dispensing: selectProduct
    HasMoney --> Idle: cancelAndRefund
    Dispensing --> Idle: dispenseComplete

Without the State pattern, every method checks the current state:

// BEFORE: every method has state checks -- this gets unmanageable fast
public class VendingMachine {
    private enum MachineState { IDLE, HAS_MONEY, DISPENSING }
    private MachineState state = MachineState.IDLE;
    private int balance = 0;

    public void insertCoin(int amount) {
        if (state == MachineState.IDLE) {
            balance = amount;
            state = MachineState.HAS_MONEY;
        } else if (state == MachineState.HAS_MONEY) {
            balance += amount;
        } else if (state == MachineState.DISPENSING) {
            System.out.println("Wait, dispensing in progress");
        }
    }

    public void selectProduct(String code) {
        if (state == MachineState.IDLE) {
            System.out.println("Insert coin first");
        } else if (state == MachineState.HAS_MONEY) {
            Product p = catalog.get(code);
            if (p != null && balance >= p.getPrice()) {
                state = MachineState.DISPENSING;
                // dispense logic...
            } else {
                System.out.println("Insufficient balance");
            }
        } else if (state == MachineState.DISPENSING) {
            System.out.println("Wait, dispensing in progress");
        }
    }
    // Every new state multiplies every method by another branch
}
# BEFORE: nested conditionals everywhere
class VendingMachine:
    def __init__(self):
        self.state = "IDLE"
        self.balance = 0

    def insert_coin(self, amount):
        if self.state == "IDLE":
            self.balance = amount
            self.state = "HAS_MONEY"
        elif self.state == "HAS_MONEY":
            self.balance += amount
        elif self.state == "DISPENSING":
            print("Wait, dispensing in progress")

    def select_product(self, code):
        if self.state == "IDLE":
            print("Insert coin first")
        elif self.state == "HAS_MONEY":
            product = self.catalog.get(code)
            if product and self.balance >= product.price:
                self.state = "DISPENSING"
            else:
                print("Insufficient balance")
        elif self.state == "DISPENSING":
            print("Wait, dispensing in progress")
// BEFORE: switch in every method
void VendingMachine::insertCoin(int amount) {
    switch (state_) {
        case IDLE:
            balance_ = amount;
            state_ = HAS_MONEY;
            break;
        case HAS_MONEY:
            balance_ += amount;
            break;
        case DISPENSING:
            cout << "Wait, dispensing in progress" << endl;
            break;
    }
}
// Repeat for selectProduct(), dispense(), cancel()...

The Solution: State Pattern

// State interface -- one method per possible action
public interface VendingState {
    void insertCoin(VendingMachine machine, int amount);
    void selectProduct(VendingMachine machine, String code);
    void dispense(VendingMachine machine);
    void cancel(VendingMachine machine);
}

// Idle: waiting for coins
public class IdleState implements VendingState {
    @Override
    public void insertCoin(VendingMachine machine, int amount) {
        machine.addBalance(amount);
        machine.setState(new HasMoneyState());
    }

    @Override
    public void selectProduct(VendingMachine machine, String code) {
        throw new IllegalStateException("Insert coin first");
    }

    @Override
    public void dispense(VendingMachine machine) {
        throw new IllegalStateException("Nothing to dispense");
    }

    @Override
    public void cancel(VendingMachine machine) {
        // Nothing to cancel in idle
    }
}

// HasMoney: coins inserted, waiting for product selection
public class HasMoneyState implements VendingState {
    @Override
    public void insertCoin(VendingMachine machine, int amount) {
        machine.addBalance(amount); // accumulate
    }

    @Override
    public void selectProduct(VendingMachine machine, String code) {
        Product product = machine.getProduct(code);
        if (product == null) {
            throw new ProductNotFoundException(code);
        }
        if (machine.getBalance() < product.getPrice()) {
            throw new InsufficientBalanceException(machine.getBalance(), product.getPrice());
        }
        machine.setSelectedProduct(product);
        machine.setState(new DispensingState());
    }

    @Override
    public void dispense(VendingMachine machine) {
        throw new IllegalStateException("Select a product first");
    }

    @Override
    public void cancel(VendingMachine machine) {
        int refund = machine.getBalance();
        machine.resetBalance();
        machine.setState(new IdleState());
        machine.returnChange(refund);
    }
}

// Dispensing: product being delivered
public class DispensingState implements VendingState {
    @Override
    public void insertCoin(VendingMachine machine, int amount) {
        throw new IllegalStateException("Dispensing in progress, please wait");
    }

    @Override
    public void selectProduct(VendingMachine machine, String code) {
        throw new IllegalStateException("Dispensing in progress");
    }

    @Override
    public void dispense(VendingMachine machine) {
        Product product = machine.getSelectedProduct();
        machine.deductBalance(product.getPrice());
        machine.deliverProduct(product);

        int change = machine.getBalance();
        if (change > 0) {
            machine.returnChange(change);
            machine.resetBalance();
        }
        machine.setState(new IdleState());
    }

    @Override
    public void cancel(VendingMachine machine) {
        throw new IllegalStateException("Cannot cancel during dispensing");
    }
}

// Context: clean delegation, no conditionals
public class VendingMachine {
    private VendingState currentState;
    private int balance;
    private Product selectedProduct;
    private final Map<String, Product> inventory;

    public VendingMachine(Map<String, Product> inventory) {
        this.inventory = inventory;
        this.currentState = new IdleState();
    }

    public void insertCoin(int amount) { currentState.insertCoin(this, amount); }
    public void selectProduct(String code) { currentState.selectProduct(this, code); }
    public void dispense() { currentState.dispense(this); }
    public void cancel() { currentState.cancel(this); }

    // Package-private state management methods
    void setState(VendingState state) { this.currentState = state; }
    void addBalance(int amount) { this.balance += amount; }
    void deductBalance(int amount) { this.balance -= amount; }
    void resetBalance() { this.balance = 0; }
    int getBalance() { return balance; }
    Product getProduct(String code) { return inventory.get(code); }
    void setSelectedProduct(Product p) { this.selectedProduct = p; }
    Product getSelectedProduct() { return selectedProduct; }
    void deliverProduct(Product p) { /* actuator logic */ }
    void returnChange(int amount) { /* coin return mechanism */ }
}
from abc import ABC, abstractmethod

class VendingState(ABC):
    @abstractmethod
    def insert_coin(self, machine: 'VendingMachine', amount: int): ...
    @abstractmethod
    def select_product(self, machine: 'VendingMachine', code: str): ...
    @abstractmethod
    def dispense(self, machine: 'VendingMachine'): ...
    @abstractmethod
    def cancel(self, machine: 'VendingMachine'): ...

class IdleState(VendingState):
    def insert_coin(self, machine, amount):
        machine.add_balance(amount)
        machine.set_state(HasMoneyState())

    def select_product(self, machine, code):
        raise InvalidOperationError("Insert coin first")

    def dispense(self, machine):
        raise InvalidOperationError("Nothing to dispense")

    def cancel(self, machine):
        pass  # Nothing to cancel

class HasMoneyState(VendingState):
    def insert_coin(self, machine, amount):
        machine.add_balance(amount)

    def select_product(self, machine, code):
        product = machine.get_product(code)
        if not product:
            raise ProductNotFoundError(code)
        if machine.balance < product.price:
            raise InsufficientBalanceError(machine.balance, product.price)
        machine.selected_product = product
        machine.set_state(DispensingState())

    def dispense(self, machine):
        raise InvalidOperationError("Select a product first")

    def cancel(self, machine):
        refund = machine.balance
        machine.reset_balance()
        machine.set_state(IdleState())
        machine.return_change(refund)

class DispensingState(VendingState):
    def insert_coin(self, machine, amount):
        raise InvalidOperationError("Dispensing in progress")

    def select_product(self, machine, code):
        raise InvalidOperationError("Dispensing in progress")

    def dispense(self, machine):
        product = machine.selected_product
        machine.deduct_balance(product.price)
        machine.deliver_product(product)
        if machine.balance > 0:
            machine.return_change(machine.balance)
            machine.reset_balance()
        machine.set_state(IdleState())

    def cancel(self, machine):
        raise InvalidOperationError("Cannot cancel during dispensing")

class VendingMachine:
    def __init__(self, inventory: dict):
        self._state = IdleState()
        self.balance = 0
        self.selected_product = None
        self._inventory = inventory

    def insert_coin(self, amount): self._state.insert_coin(self, amount)
    def select_product(self, code): self._state.select_product(self, code)
    def dispense(self): self._state.dispense(self)
    def cancel(self): self._state.cancel(self)

    def set_state(self, state: VendingState): self._state = state
    def add_balance(self, amount): self.balance += amount
    def deduct_balance(self, amount): self.balance -= amount
    def reset_balance(self): self.balance = 0
    def get_product(self, code): return self._inventory.get(code)
    def deliver_product(self, p): pass
    def return_change(self, amount): pass
class VendingMachine; // forward declaration

class VendingState {
public:
    virtual ~VendingState() = default;
    virtual void insertCoin(VendingMachine& machine, int amount) = 0;
    virtual void selectProduct(VendingMachine& machine, const string& code) = 0;
    virtual void dispense(VendingMachine& machine) = 0;
    virtual void cancel(VendingMachine& machine) = 0;
};

class IdleState : public VendingState {
public:
    void insertCoin(VendingMachine& machine, int amount) override;
    void selectProduct(VendingMachine& machine, const string& code) override {
        throw runtime_error("Insert coin first");
    }
    void dispense(VendingMachine& machine) override {
        throw runtime_error("Nothing to dispense");
    }
    void cancel(VendingMachine& machine) override { /* no-op */ }
};

class HasMoneyState : public VendingState {
public:
    void insertCoin(VendingMachine& machine, int amount) override;
    void selectProduct(VendingMachine& machine, const string& code) override;
    void dispense(VendingMachine& machine) override {
        throw runtime_error("Select a product first");
    }
    void cancel(VendingMachine& machine) override;
};

class VendingMachine {
    unique_ptr<VendingState> state_;
    int balance_ = 0;
    Product* selectedProduct_ = nullptr;
    unordered_map<string, Product> inventory_;
public:
    explicit VendingMachine(unordered_map<string, Product> inv)
        : inventory_(std::move(inv)), state_(make_unique<IdleState>()) {}

    void insertCoin(int amount) { state_->insertCoin(*this, amount); }
    void selectProduct(const string& code) { state_->selectProduct(*this, code); }
    void dispense() { state_->dispense(*this); }
    void cancel() { state_->cancel(*this); }

    void setState(unique_ptr<VendingState> s) { state_ = std::move(s); }
    void addBalance(int amount) { balance_ += amount; }
    int getBalance() const { return balance_; }
    void resetBalance() { balance_ = 0; }
    Product* getProduct(const string& code) { return &inventory_[code]; }
};

When to Use vs When to Avoid

Use State Pattern When Avoid When
Object has 3+ distinct behavioral states States are just a status field with no behavior differences
Behavior differs significantly per state All states behave the same way
State transitions follow specific rules Transitions are trivial (one direction only)
You want to eliminate state-checking conditionals A simple enum + switch is readable enough
New states might be added later The state set is tiny and fixed forever

Interview Questions

  1. โ€œHow is State different from Strategy?โ€ โ€“ In Strategy, the client chooses the algorithm. In State, the object transitions itself internally. States know about valid transitions; strategies are independent of each other.

  2. โ€œWhere does the transition logic live?โ€ โ€“ In the state objects themselves. Each state knows what transitions are valid and creates the next state. The context just calls setState().

  3. โ€œWhat about invalid transitions?โ€ โ€“ Throw an exception or return an error. The pattern makes invalid actions explicit โ€“ calling dispense() in IdleState clearly throws, rather than silently returning nothing.

  4. โ€œShould states be singletons or new instances?โ€ โ€“ If states are stateless, share them as singletons. If they carry data (like timestamps or counters), create fresh instances per transition.


See It in Action

Vending Machine Elevator Tic-Tac-Toe

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access