State

June 17, 2026 in patterns 5 minutes

A intermediate-level guide to State: before-and-after java code and diagrams for a CS student.

The “If-Else” Explosion

Imagine you are building software for a vending machine. Initially, the logic is simple: if it is idle, take money; if it has goods, dispense them. But as requirements grow—adding “out of order” modes, maintenance states, or error handling—the code begins to buckle under its own weight.

class VendingMachine {
    private int balance = 0;
    private String state = "IDLE"; // Possible: IDLE, HAS_GOODS, SOLD

    public void insertCoin(int amount) {
        if (state.equals("IDLE")) {
            balance += amount;
            System.out.println("Balance: " + balance);
            state = "HAS_GOODS";
        } else if (state.equals("HAS_GOODS")) {
            System.out.println("Already in use.");
        } else {
            System.out.println("Machine is out of order.");
        }
    }

    public void pressButton() {
        if (state.equals("HAS_GOODS")) {
            System.out.println("Dispensing item...");
            state = "IDLE";
            balance = 0;
        } else if (state.equals("IDLE")) {
            System.out.println("Insert coins first.");
        } else {
            System.out.println("Cannot press button now.");
        }
    }
}

This class suffers from “Switch Statements,” a code smell where a single method uses conditional logic to decide how to act based on an internal variable like state. As you can see in the diagram below, the behavior of insertCoin and pressButton is trapped inside large blocks that must check the current status every single time they are called.

  flowchart TD
    A[insertCoin] --> B{state == IDLE?}
    B -- yes --> C[update balance/state]
    B -- no --> D{state == HAS_GOODS?}
    D -- yes --> E[print error]
    D -- no --> F[print error]

    G[pressButton] --> H{state == HAS_GOODS?}
    H -- yes --> I[dispense/reset]
    H -- no --> J{state == IDLE?}
    J -- yes --> K[print error]
    J -- no --> L[print error]

Defining the Pain

This approach creates “conditional complexity.” The problem isn’t just that the code is long; it is that the logic is fragile. If you want to add a new state—for example, REPAIR_MODE—you cannot simply add one piece of code. You must hunt through every single method in the VendingMachine class and add another else-if branch to ensure the machine behaves correctly in that new mode.

This violates the Open/Closed Principle: your classes should be open for extension but closed for modification. Currently, every time you extend the machine’s capabilities, you are forced to modify (and potentially break) existing, working code.

The Refactor: Replace Conditional with Polymorphism

To fix this, we use a refactoring technique called Replace Conditional with Polymorphism. Instead of asking “What state am I in?” inside every method, we delegate the work to an object that represents that state.

In this design, the VendingMachine (the Context) holds a reference to a State interface. The specific behavior is moved into separate classes like IdleState and HasGoodsState. When you call insertCoin on the machine, it simply says: “Hey, current state object, you handle this.”

interface State {
    void insertCoin(VendingMachine context, int amount);
    void pressButton(VendingMachine context);
}

class IdleState implements State {
    public void insertCoin(VendingMachine context, int amount) {
        context.setBalance(amount);
        System.out.println("Balance: " + amount);
        context.setState(new HasGoodsState());
    }
    public void pressButton(VendingMachine context) {
        System.out.println("Insert coins first.");
    }
}

class HasGoodsState implements State {
    public void insertCoin(VendingMachine context, int amount) {
        System.out.println("Already in use.");
    }
    public void pressButton(VendingMachine context) {
        System.out.println("Dispensing item...");
        context.setBalance(0);
        context.setState(new IdleState());
    }
}

class VendingMachine {
    private int balance = 0;
    private State state = new IdleState();

    public void insertCoin(int amount) {
        state.insertCoin(this, amount);
    }

    public void pressButton() {
        state.pressButton(this);
    }

    // Package-private for State implementation access
    void setState(State state) { this.state = state; }
    int getBalance() {
        return balance;
    }
    void setBalance(int balance) {
        this.balance = balance;
    }
}

By moving the logic into dedicated state classes, we achieve encapsulation. The VendingMachine no longer needs to know the rules for every possible scenario; it only needs to know how to talk to the State interface.

The following class diagram shows how the machine delegates its responsibilities to these specialized objects:

  classDiagram
    class VendingMachine {
        -int balance
        -State state
        +insertCoin(int amount)
        +pressButton()
        void setState(State state)
        int getBalance()
        void setBalance(int balance)
    }
    class State {
        <<interface>>
        +insertCoin(VendingMachine context, int amount)
        +pressButton(VendingMachine context)
    }
    class IdleState {
        +insertCoin(VendingMachine context, int amount)
        +pressButton(VendingMachine context)
    }
    class HasGoodsState {
        +insertCoin(VendingMachine context, int amount)
        +pressButton(VendingMachine context)
    }

    VendingMachine --> State : uses
    IdleState ..|> State : implements
    HasGoodsState ..|> State : implements

How the Handshake Works

The magic happens during runtime. When a user interacts with the machine, a “handshake” occurs where the machine passes itself as an argument to the state object. This allows the state object to trigger transitions—like telling the machine to switch from IdleState to HasGoodsState.

  sequenceDiagram
    participant VM as VendingMachine
    participant IS as IdleState
    participant HGS as HasGoodsState

    Note over VM, HGS: User inserts a coin
    VM ->> IS: insertCoin(this, amount)
    IS ->> VM: setBalance(amount)
    IS ->> VM: setState(new HasGoodsState())
    Note right of VM: The machine has now changed behavior!

This implementation is essentially a code-level version of a Finite State Machine (FSM). Each class represents a discrete node in the machine’s lifecycle, and the methods define the transitions between those nodes.

When to Use It, and When Not To

The State pattern is powerful, but it comes with a cost: “class explosion.” Because every new state requires a brand-new class file, you can end up with dozens of tiny classes that only exist to hold a few lines of logic.

Use the State pattern when:

  • An object’s behavior depends on its current status AND is complex enough that if/else blocks are becoming unreadable.
  • You find yourself copying and pasting similar conditional checks across multiple methods in the same class.
  • Transitions between states involve significant logic or side effects.

Avoid the State pattern when:

  • The object only has two possible states (e.g., ON and OFF) that rarely change. A simple boolean flag is much more efficient and readable there.
  • You are working in a context where creating many small classes adds unnecessary overhead or complexity to the build process without providing clear benefits in readability.

Takeaways

  • Replace messy if/else chains with polymorphic calls to specialized state objects.
  • Use this pattern when an object’s behavior changes significantly based on its internal status.
  • Refactoring to State makes your code easier to extend because adding a new state only requires adding a new class, not modifying existing logic in the main class.

Usage

Using the refactored VendingMachine context

VendingMachine vm = new VendingMachine();
vm.insertCoin(50);
vm.pressButton();