Machine-coding case study
Parking lot
A parking lot issues a ticket on park and a receipt on exit. Allocation (which free spot fits this vehicle) and pricing (how much the stay costs) are strategies. Vehicle types come from a factory. Occupy must be atomic so two gates cannot take the same spot. The one-file program that parks a car becomes a knot once EV (electric vehicle) spots and airport fees arrive.
The real question is not "can I store spots in a list?" It is: can two gates take the same spot, and can I add a pricing rule without editing ParkingLot?
Interview path
Name the nouns
Name lot, floor, spot, vehicle, and ticket. Money and Duration are values, not entities.
Put decisions on seams
Put allocation and pricing on strategy seams. Create vehicle types through a factory.
Protect occupy
Choose-and-mark must be one atomic step. Checking isFree then occupy is a race.
What actually happens when requirements keep coming
After the first park/exit demo, the next requirement is almost always a new vehicle type or a new fee. If ParkingLot contains if (type == EV) and if (airport), every follow-up edits the lot. That is the smell you are designing against.
Allocation is a decision that needs free spots that fit the vehicle. is a decision that needs time and a policy. Ticket creation needs an id generator and a Clock. If ParkingLot does all three with switches, the follow-up edits the lot. If each is a strategy (a swappable decision object) or factory (creates typed objects without switch soup), the lot stays boring, which is the point.
EV spots arrive
New spot type plus a filter in the allocation strategy. ParkingService.park does not grow a boolean.
Airport vs mall fees
Swap the PricingStrategy at construction. Tests freeze Clock so a two-hour stay is deterministic.
Class and responsibility walk
Once the story is clear, name the nouns that keep identity. Money and Duration do not. The public API stays tiny: park returns a ticket; exit returns a receipt. Everything else is a collaborator. See domain modeling and information expert.
| Type | Responsibility | Owns |
|---|---|---|
| ParkingLot | Owns floors and the public surface. Does not pick a pricing formula. | Floors, entry to park/exit |
| Floor | A slice of spots. Answers which spots are free on this level. | ParkingSpot collection |
| ParkingSpot | One rectangle of concrete. Protects the one-vehicle invariant. | id, type, occupy/vacate |
| Vehicle | Identity of what arrived. Type decides which spots fit. | plate, VehicleType |
| Ticket | Proof of a stay. Issued at park, closed at exit. | id, spot, entry Instant, vehicle |
| ParkingService | Thin controller for park and exit. Collaborators own the decisions. | allocation, pricing, clock |
Park path and Java API sketch
With nouns named, walk the happy path. Gate A does not rummage through floors. It asks ParkingService. The service asks the allocation strategy, then asks the spot to occupy itself. Ticket minting happens only after occupy succeeds.
Read the diagram definition
sequenceDiagram
autonumber
participant Gate as Gate A
participant Lot as ParkingService
participant Alloc as AllocationStrategy
participant Spot as ParkingSpot
participant Clock as Clock
Gate->>Lot: park(vehicle)
Lot->>Alloc: choose(free spots, vehicle)
Alloc-->>Lot: Optional spot 101
Lot->>Spot: tryOccupy()
alt occupied atomically
Lot->>Clock: now()
Lot-->>Gate: Ticket
else already taken
Lot-->>Gate: retry or LotFull
endSeams in code
The sketch below is the seams, not a finished product. ParkingSpot owns the compare-and-set (CAS: one atomic check-and-update). ParkingService does not set a boolean on a HashMap of spots.
interface SpotAllocationStrategy {
Optional<ParkingSpot> choose(List<ParkingSpot> free, Vehicle vehicle);
}
interface PricingStrategy {
Money fee(Ticket ticket, Instant exitTime);
}
interface VehicleFactory {
Vehicle from(VehicleType type, String plate);
}
class ParkingService {
private final SpotAllocationStrategy allocation;
private final PricingStrategy pricing;
private final Clock clock;
Ticket park(Vehicle vehicle) { /* choose, occupy, issue */ }
Receipt exit(TicketId id) { /* fee, free spot, close ticket */ }
}
class ParkingSpot {
private final AtomicReference<SpotStatus> status =
new AtomicReference<>(SpotStatus.FREE);
boolean tryOccupy() {
return status.compareAndSet(SpotStatus.FREE, SpotStatus.OCCUPIED);
}
boolean vacate() {
return status.compareAndSet(SpotStatus.OCCUPIED, SpotStatus.FREE);
}
}Concurrency: two gates, one spot
The sketch assumes occupy is safe. The invariant is one vehicle per spot. Gate A and Gate B both seeing spot 101 free is a check-then-act race. Occupy must be one step: compare-and-set, or a lock around choose-and-mark. Say that story before you type synchronized. Same lesson as thread safety and race conditions.
Read the diagram definition
sequenceDiagram
participant A as Gate A
participant S as Spot 101
participant B as Gate B
A->>S: isFree()? true
B->>S: isFree()? true
A->>S: occupy()
B->>S: occupy()
Note over S: Two cars, one rectangle of concreteAfter a failed tryOccupy, allocation may retry with remaining free spots. Do not mark 101 occupied in the strategy and again in the spot: the spot is the lock.
Invalid cases and what the follow-up changes
Atomic occupy is necessary but not enough. These cases must fail cleanly, and the usual interview extensions should land as new classes, not new booleans on ParkingLot.
Invalid cases the design must reject
Park a motorcycle into a taken compact spot
tryOccupy fails. Do not overwrite the occupant or mint a second ticket for 101.
Exit with an unknown ticket id
No receipt. The lot does not invent a stay. Return a domain error, not a null fee.
Exit a ticket twice
Second exit is illegal. Ticket is CLOSED. Idempotent already-closed is a follow-up.
EV that needs a charger, only compact free
Allocation returns empty. Do not downcast the vehicle into a non-charging spot.
Negative duration because Clock went backwards
Injected Clock should be monotonic in tests. Pricing must not emit negative Money.
What the follow-up changes
Tests freeze the clock so a two-hour stay is deterministic. The extension they always add (a new vehicle type or a new fee rule) should land as a new class, not a new boolean on ParkingLot.
Handicap or compact-only floors
Filter in allocation. Floor still only lists spots. Do not add Floor.handicapMode.
First hour free, then hourly
New PricingStrategy. Ticket still only stores entry Instant. Fee math stays off the ticket.
Valet that parks anywhere
Different allocation strategy that ignores nearest-to-elevator. Same park() signature.
Receipt printing / SMS
Side effect after exit. Not a method on ParkingSpot. A listener or thin adapter outside the domain.
Wrap-up
You have the nouns, the seams, the race, and the rejects. After EV and airport pricing, a one-file program is a knot. Put allocation and pricing on strategy seams. Create vehicle types through a factory. Occupy is compare-and-set. Clock is injected. ParkingLot stays boring on purpose.
Key checks
- · Retry allocation after a failed tryOccupy so another free spot can still win.
- · Negative duration must not emit negative Money; keep Clock monotonic in tests.
- · Receipt SMS is a side effect after exit, not ParkingSpot.print().
- · First hour free is a new PricingStrategy, not a field on Ticket.
Read next
You're done when
Two gates cannot both occupy the same spot (tryOccupy / CAS wins for one). Park returns a ticket only after occupy succeeds. Exit returns a receipt. A new fee rule or vehicle type lands as a strategy or factory class, not a new if inside ParkingLot.