Published

- 7 min read

State Pattern in Java: Why an ATM Is Not the Strategy Pattern

One class per state, each one returning the next, and the state machine you already run in production.

img of State Pattern in Java: Why an ATM Is Not the Strategy Pattern

Your interviewer asks: an ATM behaves differently at every step, so is it the Strategy pattern?

If you said yes, that’s the trap. An ATM does change its behaviour: with no card it waits for one, with a card it waits for a PIN, with a PIN it lets you withdraw. But nobody switches it from outside. It changes on its own, and that is the State pattern. It has the same class diagram as the Strategy pattern, which is exactly why interviewers ask for the difference.

The if-else version

Most of us would code it like this. The ATM remembers its state, and every method checks it with an if-else:

   void insertCard() {
  if (state == IDLE) {
    state = HAS_CARD;
  } else if (state == HAS_CARD) {
    say("card already in");
  } else if (state == HAS_PIN) {
    say("finish first");
  }
}

enterPin() and withdraw() have the same if-else. That’s fine until you add a state like out of cash:

   void insertCard() {
  if (state == IDLE) {
    state = HAS_CARD;
  } else if (state == HAS_CARD) {
    say("card already in");
  } else if (state == HAS_PIN) {
    say("finish first");
  } else if (state == NO_CASH) {
    say("out of cash");
  }
}

Now you’re changing every method, and each one has to get the new branch right. The rules of one state are spread across all of them: to see what HAS_PIN allows, you read three if-else chains.

The State pattern

Turn it around. Each state gets its own class, behind one interface:

   interface AtmState {
  AtmState insertCard();
  AtmState enterPin();
  AtmState withdraw();
}

Every method returns the state the ATM should be in next. The ATM just hands the work to its current state and keeps whatever comes back:

   class Atm {
  AtmState state = new Idle();

  void insertCard() {
    state = state.insertCard();
  }
  // enterPin, withdraw: the same
}

Each state decides what comes next. Idle moves on when a card goes in, and ignores the rest:

   class Idle implements AtmState {
  public AtmState insertCard() {
    return new HasCard();
  }
  public AtmState enterPin() {
    return this;
  }
  public AtmState withdraw() {
    return this;
  }
}

return this means “not allowed here, stay where you are”. HasCard and HasPin have the same shape. Out of cash? One more class:

   class NoCash implements AtmState {
  public AtmState insertCard() {
    return this;
  }
  // enterPin, withdraw: the same
}

Here’s the honest part: one existing line does change, because somebody has to send the ATM there. HasPin returns NoCash when the last notes leave:

   public AtmState withdraw() {
  return --Atm.notes > 0
      ? new Idle()
      : new NoCash();
}

Atm, Idle and HasCard stay as they are, and no method grows a branch. Run it and watch the ATM move through its states:

/CodeVxDev

ionicons-v5-d

state

java State.java

7:21:30

withdraw(): Idle stays
insertCard(): Idle -> HasCard
insertCard(): HasCard stays
enterPin(): HasCard -> HasPin
withdraw(): HasPin -> NoCash
insertCard(): NoCash stays

Strategy vs State

This looks just like the Strategy pattern: one interface, a class per behaviour, and an object that holds one of them. The difference is who swaps:

   // Strategy: the caller picks
checkout.strategy = payPal;

// State: it moves itself
state = state.insertCard();

In Strategy, your code picks how a job gets done, like how to pay at a checkout. In State, nobody picks. The states switch on their own:

/CodeVxDev

ionicons-v5-d

state

java WhoSwaps.java

7:21:30

PayPal pays 4900
Idle -> HasCard
HasCard stays

This is the comparison interviewers ask for:

StrategyState
Who changes the objectThe caller (or the DI container), onceThe object itself, again and again
The classes know othersNo, strategies are independentYes, a state names the state that follows
The question it answers“Which way of doing this job?”“What am I allowed to do right now?”
The one linecheckout.strategy = payPal;state = state.insertCard();

One system can use both. In an elevator system, the dispatcher that decides which car answers a call is a Strategy (nearest car, SCAN, zoned), while each car moving between idle, moving and doors open is a State.

You’ve probably built this in production

An order gets created, then paid, then shipped. You can cancel before it ships, but after that it’s too late. That is a state machine, and when it’s this simple, skip the classes and pack it into an enum:

   enum Status {
  CREATED, PAID, CANCELLED,
  SHIPPED {
    Status cancel() {
      throw new TooLate();
    }
  };
  Status cancel() {
    return CANCELLED;
  }
}

Each constant is a state, and SHIPPED overrides cancel() with its own rule, so there’s no if-else on the status anywhere. TooLate is a small RuntimeException of our own.

/CodeVxDev

ionicons-v5-d

state

java OrderStatus.java

7:21:30

CREATED.cancel() -> CANCELLED
PAID.cancel() -> CANCELLED
CANCELLED.cancel() -> CANCELLED
SHIPPED.cancel() throws TooLate

Many codebases go lighter still: the enum only says which status may follow which, and one method on the order checks every move:

   enum Status {
  CREATED, PAID, SHIPPED, CANCELLED;

  boolean canMoveTo(Status next) {
    return switch (this) {
      case CREATED -> next == PAID
          || next == CANCELLED;
      case PAID -> next == SHIPPED
          || next == CANCELLED;
      case SHIPPED, CANCELLED -> false;
    };
  }
}

class Order {
  Status status = Status.CREATED;

  void moveTo(Status next) {
    if (!status.canMoveTo(next)) {
      throw new IllegalStateException(
          status + " can't go to " + next);
    }
    status = next;
  }
}

/CodeVxDev

ionicons-v5-d

state

java OrderFlow.java

7:21:30

now SHIPPED
refused: SHIPPED can’t go to CANCELLED

That version is a transition table, which suits a status that only gates moves. When each state also behaves differently, what withdraw() or cancel() actually does, give the behaviour to the state: constants with bodies, or a class per state.

Before you ship it

A few things the short version leaves out:

  • Shared instances. new HasCard() on every call is wasteful. These states hold no data, so share one instance of each, as constants or as enum constants like the ones above.
  • Say no out loud. return this quietly ignores a call that isn’t allowed. In production, reject it with a clear message or an exception, so a client that enters a PIN with no card learns why nothing happened.
  • Store the state, not the class. The state is a column: store the enum (@Enumerated(EnumType.STRING) in JPA) and hang the behaviour off it. Stateless state classes can be shared instances, or Spring beans looked up by status.

Spot it in LLD interviews

If the requirements read like a lifecycle (“first…, then…, only after…, can’t… once…”) and the same action does different things at different steps, model the steps as states:

LLD questionThe object with a lifecycleStates
Vending machineThe machineIdle, has coin, dispensing, sold out
Elevator systemOne carIdle, moving up, moving down, doors open
Traffic signalOne lightRed, green, yellow
Movie or seat bookingA seatAvailable, held (with a timeout), booked
Task boardA taskTo do, in progress, in review, done
Food deliveryAn orderPlaced, accepted, cooking, picked up, done
Parking lotA ticketIssued, paid, exited
Document approvalA documentDraft, in review, approved, published

In the interview, draw the states and the arrows between them first. Then show one state class, and say how a new state gets added: one new class, plus the one state that leads to it.

When not to use it

State earns its keep when an object moves through stages with rules of their own. With two states and a boolean, an if-else is simpler, and a class per state is just noise.

/CodeVxDev

ionicons-v5-d

state

cat state.md

7:21:30

changes its own behaviour
use: stages with their own rules
skip: two states and a boolean

The interview trap

An order just got paid, and email, inventory and analytics all need to know. Should the order call all of them and notify?

A. Yes, call them    B. No

Think about what happens when a fourth service needs to know. Leave A or B as a comment on the video; the next episode in this series answers it.