# Parking Lot — 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.**

Ten classes, none over ~75 lines. `ParkingLot` holds no rule that a requirement could reach.

| File | One reason to change |
|---|---|
| `ParkingLot` | the *sequence* of admitting and releasing changes |
| `SpotGrid` | how occupancy is stored |
| `SpotAllocator` / `FirstFitAllocator` | where a vehicle should go |
| `PricingPolicy` / `FlatHourlyPricing` | what a stay costs |
| `Footprint` | how much room a kind of vehicle takes |
| `Stay`, `Occupancy` | what we record about a vehicle in the lot |
| `Demo` | what we show when we run it |

---

## Seam 1 · `PricingPolicy` — the tariff is injected, not written down

**Anticipates:** any change to money. First hour free, weekend rates, EV discounts, a per-type
rate change, tiered long-stay pricing. This is the single most volatile requirement in the problem
— every parking lot on earth has changed its tariff and none has changed what a spot is.

**Cost without it.** With `if (type == CAR) fee = 2000 * hours;` inside `unpark`, a new tariff is
an edit to the method that also validates tickets, releases spots and builds receipts. Curveball 2
would be ~15 lines rewritten in the class that must not break, with no way to keep the old tariff
alongside the new one for last month's reports. **With it, curveball 2 is a new file and one line
of wiring: measured at 2 lines.**

**Why the rate table is a `Map<VehicleType, Long>` and not a `switch`.** A switch on type inside
`feeMinor` is behaviour selected by a type field — the D3 level-0 anchor. As data, the tariff can
be printed, tested and eventually loaded from configuration without touching code.

**Why `startedHours` is a static method on the interface.** Every tariff bills in the same unit;
what varies is the rate, not the clock. Putting it on the interface shares it without an abstract
base class that would then own the rounding *and* the rates (composition over inheritance).

## Seam 2 · `SpotAllocator` — placement is a policy, and it is *pure*

**Anticipates:** any change to where vehicles go. Motorbikes sharing a spot, nearest-to-lift,
reserved bays, EV chargers, disabled bays, per-floor packing.

**Cost without it.** With the search loop inline in `park`, curveball 1 (two motorbikes to a spot)
means editing the loop, the availability count and the release path — three touchpoints in the
class that also issues tickets — and the old rule is gone rather than composed with. **With it,
curveball 1 is one new class that wraps the old one and one line of wiring: measured at 2 lines.**

**`allocate` returns the spots and changes nothing.** This is the decision worth defending. The
lot applies the allocator's answer, so the same call answers "where *would* the next car go?"
without parking a car. That is why curveball 3's new `nextSpotFor` is a one-line body: 4 lines
measured, all of them the new method's own declaration. Had `allocate` mutated the grid, the same
requirement would have needed a second search path — a duplicate of the placement rule, which is
exactly the duplication that goes stale.

**Why `capacityFor` lives on the allocator, not the lot.** "How many more fit?" is the same
question as "where does the next one go?", asked in bulk. Splitting them across two classes is how
they drift: bikes that can share would be admitted by one and denied by the other.

## Seam 3 · `SpotGrid` stores, and decides nothing

A spot holds a **list** of occupants although today's rules never put two vehicles on one spot.
Deliberate: *"one vehicle per spot"* is a rule the allocator enforces, not a property of concrete.
Motorbikes sharing is a real requirement in real lots, and the alternative — `Map<String, Vehicle>`
— pushes curveball 1 into the storage class, the allocator and the release path at once.

This is the one place where the reference spends something up front for a change it expects. The
honest defence is the cost: one field type, no extra class, no extra interface.

## Identity · occupancy is tracked by ticket id, never by `Vehicle`

`Vehicle` is a value — two bikes registered `"BIKE1"` are `equals` — so `list.remove(vehicle)`
would free whichever occupant happened to compare equal. `Occupancy` carries the **ticket id**,
which is the identity of the stay, and `vacate` removes by that. Entity versus value, decided
rather than stumbled into. `Stay` is an entity for the same reason and is looked up only by ticket
id.

## Invariants sit in constructors

`SpotGrid` refuses a lot with fewer than one spot. `Stay` refuses to exist without at least one
spot and copies the list it is given. `Occupancy` refuses nulls. There is no `validate()` method
anywhere, because an invalid instance never gets far enough to need one.

## Collections never escape

`spotIds()` returns an immutable list built once with `List.copyOf`. `occupantsOf()` returns a
snapshot. An allocator can scan the lot and cannot corrupt it — which matters precisely because
allocators are the part a candidate is expected to replace.

## Time is a parameter, never a clock

`park` and `unpark` take the `Instant`. Nothing in the reference calls `Instant.now()`, so every
fee is reproducible and no test needs to sleep or freeze a clock. If a future requirement needs
the current time, the injection point is the caller, not a field.

## Errors: `Optional` inside, exceptions at the edge

Internally `allocate` returns `Optional<List<String>>` — "no room" is an ordinary outcome of a
search, not an exception. At the API edge it becomes `IllegalStateException`, because the
*caller's* request is impossible. That is the boundary being drawn: optionals for expected
absence, exceptions for a broken request.

**Why no custom exception types.** The contract fixes the exception classes the grader asserts, so
`NoSpotAvailableException extends IllegalStateException` would add two files and buy nothing the
message does not already say. If the contract had left the choice open, the answer would be
different — and that, rather than a rule, is the thing to say out loud.

## What was deliberately *not* built

Named so the scope is explicit rather than forgotten:

- **No `Floor` and no `SpotSize`.** One floor and one spot size is what the requirement says. A
  floor abstraction with a single implementation is the D3 level-3 penalty (speculative interface),
  not sophistication.
- **No locks.** The contract states the lot is called from one thread. A `synchronized` on every
  method would be cargo cult; the right move is to say "here is the shared mutable state —
  `openStays` and the grid — and here is where I would put the lock if the gates were concurrent."
- **No persistence, no payment, no reservations.** Stated out of scope up front.
- **No `Vehicle` subclasses.** `Footprint` carries the only behaviour that differs, and a
  `Motorbike extends Vehicle` hierarchy would put allocation rules on data objects that cross the
  API boundary as records anyway.
