# Vending Machine — decision log

The Phase 5 answer key. Every entry is a question an interviewer actually asks ("why is that a
separate class?") and the answer that earns the mark: **what change the seam anticipates, and what
the change would cost without it.**

Thirteen classes, none over ~130 lines and most under 40. `VendingMachine` holds the *sequence* of
a sale and no rule that a requirement could reach.

| File | One reason to change |
|---|---|
| `VendingMachine` | the *sequence* of a sale changes |
| `MachineFactory` | what a standard machine is made of changes |
| `TransitionTable` | how transition rules are indexed changes — i.e. never again |
| `Transitions` | what the machine is allowed to do changes |
| `Transition`, `MachineEvent` | what a rule is made of changes |
| `SlotRack` | how stock is stored changes |
| `CoinPurse` | how coins are stored changes |
| `ChangeMaker` / `GreedyChangeMaker` | which coins pay out an amount changes |
| `PricingPolicy` / `ListPricing` | what the till charges changes |
| `Demo` | what we show when we run it changes |

---

## Seam 1 · `TransitionTable` — the state machine is a lookup, not a control flow

**Anticipates:** any change to what the machine is allowed to do. A service mode, an out-of-order
mode, a test mode, a pre-paid mode, a "please take your item" timeout. All of these are rows.

**The one decision to defend, if there is time for only one.** The obvious implementation is
`switch (state)` — or a `boolean` per mode — inside each of the four operations. Three things go
wrong with it, in increasing order of seriousness:

1. The rules spread across four methods, so *"what can the machine do while it is dispensing?"* has
   no single place to read. The requirement was a table; the code should be one.
2. Each method grows a guard clause that has to be kept consistent with three others by hand.
3. **An absent rule stops being a refusal.** In a table, a state with no outgoing edge for an event
   rejects it — for free, with no code written anywhere. That is the property that makes a whole new
   state cheap.

**Cost without it.** Curveball 3 asks for a maintenance mode that rejects every customer operation
and is only reachable from idle. With guard clauses that is four edits to the four operations, plus
two `if (state != IDLE) throw` restatements inside the two new methods, plus a compile error at
every exhaustive `switch (state)` because the enum gained a constant. **With the table it is two rows
in a new file and one line of wiring — and the requirement's four separate rules are enforced by
nothing at all:**

| The requirement said | Where it is enforced |
|---|---|
| out of service it accepts no coins, selections, collections or refunds | nowhere — `MAINTENANCE` has no rows for those events |
| it can only go out of service when idle | nowhere — the only row pointing at `MAINTENANCE` starts at `IDLE` |
| going out twice, or back when never out, is refused | nowhere — same reason |
| coming back leaves the stock and coins as they were | nowhere — a transition moves the state and touches nothing else |

**Why the rules live in `Transitions`, not inside `TransitionTable`.** They change for different
reasons. The table is an indexing mechanism and is finished; the rows change every time the machine
learns a new way to behave. Splitting them is what makes curveball 3 a *new file* rather than an
edit — the difference between D4 level 2 and level 3.

**Why `EnumMap` keyed on the class rather than a `switch`.** A `switch` covering every constant of
`MachineState` stops compiling when a constant is added. That is normally a feature and it is worth
saying so out loud rather than pretending otherwise. The reason it is not needed here is that the
decision is *already* forced, and forced in a better place: a new state that no transition mentions
can never be entered, so an incomplete table is not silently wrong either. `EnumMap` gets the safety
without the edits.

**Why `next()` is pure.** It looks up the target and changes nothing, so every operation can ask
"is this legal?" *before* doing any of the work it implies. See the commit ordering below — that
purity is what makes it possible.

## Seam 2 · `MachineFactory` — construction is a job, and it has an owner

**Anticipates:** everything about what a machine *is*, as opposed to what it *does*. New products,
new prices, a bigger float, a new denomination in the float, a promotion, a new mode of operation.

**The decision worth defending: `VendingMachine` has no convenience constructor.** Every collaborator
is injected and there is deliberately no `VendingMachine()` that supplies its own defaults. That
looks like extra ceremony until you ask where a requirement lands. All three curveballs are changes
to *what the machine is made of*, and none of them opens the class that runs the sale:

| Curveball | Where it landed | Measured cost |
|---|---|---|
| a new coin denomination | nowhere at all | **0 lines** |
| a promotional tariff | one line in the factory + one new file | **2 lines** |
| a maintenance mode | one line in the factory, two words in an enum, two new interface methods, one new file | **8 lines**, 6 of them declaration |

Had the float and the slot list sat inside a `VendingMachine(…)` convenience constructor, all three
would have edited the class that validates state, empties escrow and refuses five different ways.
That is the whole argument, and it is measurable rather than aesthetic.

**Why the parts are exposed as well as the whole.** `standardSlots()` and `standardFloat()` are
public so a variation on the standard machine reuses what it is not changing instead of copying it.

## Seam 3 · `ChangeMaker` — pure, so a sale can be refused before it happens

**Anticipates:** a different change algorithm (a complete search when greedy is not enough for a coin
set), a rule about preferring to hand out small coins, a machine that keeps a reserve.

**`compose` reads the purse and changes nothing.** This is the decision that makes the requirement
*"a refused sale leaves the machine exactly as it was"* free rather than expensive. Because the
question *"could this change be paid, if we banked what was just inserted?"* is answerable without
banking anything — `hopper.combinedWith(escrow)` builds a third purse and mutates neither — the
`ExactChangeRequiredException` is thrown before a single coin has moved. A version that paid the
change out and then rolled back would need a rollback path, which is a second copy of the sale logic
running backwards, and those go stale.

**It answers "cannot" with an empty `Optional`, and the machine turns that into an exception.** That
boundary is deliberate and is the D2 answer: failing to find a combination is an ordinary outcome of
a search, so internally it is an `Optional`. At the API edge it becomes an exception, because from
the *customer's* point of view the request could not be honoured. Optionals for expected absence,
exceptions for a request that cannot be served.

**Why the denominations are derived, not written down.** `Arrays.stream(Coin.values()).sorted(by
minorValue, reversed)` — nothing in the class names a nickel or a dollar, and nothing depends on the
order the constants happen to be declared in. That is the entire reason curveball 1 costs **zero
lines**. Relying on declaration order would have worked here by luck; sorting explicitly does not
need the luck.

## Seam 4 · `PricingPolicy` — the shelf label and the till are two different things

**Anticipates:** any change to money that is not a change to the rack. A promotion, a happy hour, a
staff discount, a loyalty rate, tax.

`Slot.priceMinor()` is the price *printed on the rack* — data about the product. What the till asks
for is a *decision*. They are equal today, which is exactly why collapsing them is tempting.

**Cost without it.** Reading `slot.priceMinor()` at the point of sale puts the discount arithmetic,
the rounding rule and the free-slot exception inside `select()`, next to the funds check whose meaning
they change. Worse, there would be nowhere to express *"the shelf label still says 65"*, because there
would only be one number. **With it, curveball 2 is a decorator and one line of wiring: measured at
2 lines.**

This is the thinnest seam in the design and the honest defence is the cost: one interface, one
one-line implementation. A single-implementation interface is the D3 level-3 penalty when there is no
foreseeable second one — and a vending machine that never runs a promotion has not been invented.

## The commit discipline · ask, act, commit

Every operation in `VendingMachine` is the same three steps:

1. **Ask.** `transitions.next(state, event)` — is this legal from here? Changes nothing, and returns
   the target state to use later.
2. **Act.** Work out the whole outcome — slot, price, change — throwing if any of it is impossible.
   Still nothing has changed.
3. **Commit.** Move the money, the stock and the state, in that order, with nothing left that can
   fail.

This is why all five refusals leave the machine untouched, and it is a *property of the ordering*
rather than of any try/catch. Note that `state = next` is the last line of every operation, not the
first — assigning it early and undoing it on failure is the version that has bugs.

The precedence of the five refusals falls out of this ordering rather than being encoded separately:
state, then slot existence, then stock, then funds, then change. Each check is where its knowledge
lives — the table knows about states, the rack knows about codes and stock, the policy knows the
price, the change maker knows the coins — and the order they run in *is* the requirement's order.

## Errors · one hierarchy, five leaves, and none of them `IllegalStateException`

The contract fixes the exception types because the grader must name what it expects, but the shape is
the shape a good answer would have chosen, and the reasons are worth having ready:

- **Five types, not one with five messages.** Callers act differently on them. A display says "SOLD
  OUT" for one and "INSERT 40 MORE" for another; an operator's alert system watches only for
  `ExactChangeRequiredException`, because that one means *go and refill the hopper*. One type would
  force every caller to parse strings.
- **A common abstract root**, so a caller that does not care can catch one thing, and so
  `catch (VendingMachineException)` is exhaustive over the domain. Abstract because *"something went
  wrong with the vending machine"* never happens — one of the five specific things does.
- **They carry data, not just prose.** `InsufficientFundsException` knows the price, the amount
  inserted and derives the shortfall, because the display needs the number and computing it twice in
  two places is how the two drift apart.
- **`IllegalTransitionException` is a domain exception, not `IllegalStateException`.** A customer
  pressing a button the machine will not honour is an ordinary event in a machine's day. `unpark` in
  the parking lot problem is told to throw `IllegalArgumentException` for a bad ticket, and that is
  right *there* because the contract fixed it; here the contract chose the other way, and the reason
  is that these five refusals are all the same kind of thing and belong in one hierarchy where a
  caller can handle them as a set.

## Identity and values

`Slot` is an immutable **snapshot** identified by its `code`, never by equality of its contents.
Selling one item replaces the value stored under the code rather than mutating it, so a snapshot
already handed to a caller stays true to the moment it was taken. Nothing ever looks a slot up by
anything but its code — `List<Slot>.remove(slot)` would find whichever slot happened to compare
equal, and slots whose stock has changed are no longer equal to themselves.

## `CoinPurse` is one class used twice, and that is the point

There are two purses in a machine: the **hopper** (coins the machine owns and pays change from) and
the **escrow** (coins a customer has inserted and could still ask back). They are the same thing
twice, and modelling them as one class is what makes a sale readable — completing one is literally
*"drain the escrow into the hopper, then take the change out of the hopper"*.

Escrow is the answer to the question the problem is really about: **whose money is it?** Until the
sale completes it is the customer's, which is why `refund()` can promise to return *the very coins
that were inserted* and can never fail. A design that banked coins on insertion has to reconstruct a
refund out of whatever it happens to hold, and can then fail to refund — a machine that eats money.

`removeAll` checks on a copy and commits at the end, so a purse can never be left half-emptied by a
payout it could not make. Same discipline as the machine, one level down.

## Collections never escape

`slots()` returns `List.copyOf`. `holdings()` returns `Map.copyOf`. `contents()` builds a fresh
immutable list. `TransitionTable` copies both the outer map and every row at construction, so the
rules cannot be edited after the machine is built. `DispenseResult` copies the change list in its
compact constructor. A caller can read everything and corrupt nothing — which matters most for
exactly the parts a candidate is expected to replace.

## Time is absent, deliberately

There is no clock anywhere: nothing in this problem depends on when it happens. Worth saying out
loud rather than injecting a `Clock` that nothing reads — a speculative dependency is the same
mistake as a speculative interface. If "happy hour" had meant *between 4pm and 6pm* rather than
*from now on*, the clock would be a constructor parameter of the pricing policy and of nothing else,
which is exactly why the tariff is behind a seam.

## What was deliberately *not* built

Named so the scope is explicit rather than forgotten:

- **No restocking and no hopper refill.** Stated out of scope. They are the operator's API, and the
  natural home for them is a second interface rather than more methods on this one, so that a
  customer-facing caller cannot reach them (interface segregation).
- **No locks.** The contract states one customer at a time. `synchronized` on every method would be
  cargo cult; the right move is to name the shared mutable state — `state`, `tray`, the two purses
  and the rack — and say where the lock would go if the machine had a network interface.
- **No `Product` class.** A slot's `productName` is a string because nothing in this problem does
  anything with a product except print its name. A `Product` entity with one field is a class earning
  nothing.
- **No sealed hierarchy for the states.** `MachineState` is an enum, and the transition table is the
  behaviour. A `sealed interface State` with `permits Idle, AcceptingMoney, Dispensing` and a
  `handle()` method per state is the other legitimate answer — it puts the behaviour in the states
  instead of in a table. It costs one class per state, moves the rules back out of one place into
  four, and makes "what can it do while dispensing?" a matter of reading a class rather than a row.
  The table wins here because the rules are the volatile part and the states are not; the answer
  would flip if each state carried substantial behaviour of its own.
- **No `Optional` at the API edge.** `select` throws rather than returning
  `Optional<DispenseResult>`, because there are five distinct reasons for failure and an empty
  `Optional` can carry none of them.
