Object-Oriented Design Interview

Course Content

Object-Oriented Design Interview

14 sections · 29 lessons

Parking Lot: spot assignment, patterns and the concurrency question


The model is on the board: floors that own their availability, a ticket that records the session, and pricing behind an interface. The last twenty minutes test whether it works and whether it was built for change — the spot-assignment code where the design shows, the patterns and why, the extension questions, and what happens when two gates reach the last free spot at once.

Spot assignment, in code

This is the method worth writing. It touches both non-functional requirements — the lookup speed and the double-assignment risk — so it is where a good model produces short code and a bad one produces a mess.

Finding a free spot without scanning the lotSSMLSMMLSMLLSSMLL1L2L3L4One free-spot queue per size, so assignment is a pop, not a search.
A medium car may take a large spot, so the fit test walks up sizes rather than branching on type.

The naive version, and why it fails

Start here, because it is what most first attempts produce:

Java
public Ticket park(Vehicle vehicle) {    for (Floor floor : floors) {        for (ParkingSpot spot : floor.getSpots()) {          // scans everything            if (!spot.isOccupied() && spot.getSize().fits(vehicle.getSize())) {                spot.assign(vehicle);                return new Ticket(nextId(), vehicle, spot, clock.now());            }        }    }    throw new NoSpotAvailableException(vehicle.getSize());}

Two problems, both scored.

It scans. A 2,000-spot lot that is nearly full walks almost every spot on every entry. That violates requirement N1.

It is not safe under two threads. Between isOccupied() returning false and assign() running, another gate's thread can assign the same spot. Both cars are told to park in bay C-114. The extensions at the end of this lesson fix this properly.

The availability index

Keep the free spots of each size in a queue per floor, and the lookup becomes a poll.

Java
public class Floor {    private final int floorNumber;    private final Map<SpotSize, Deque<ParkingSpot>> free = new EnumMap<>(SpotSize.class);    public Optional<ParkingSpot> claimFree(SpotSize size) {        Deque<ParkingSpot> queue = free.get(size);        ParkingSpot spot = (queue == null) ? null : queue.pollFirst();        return Optional.ofNullable(spot);    }    public void release(ParkingSpot spot) {        free.get(spot.getSize()).addLast(spot);    }    public int countFree(SpotSize size) { return free.get(size).size(); }}

claimFree removes the spot from the free queue as it returns it, so the same spot cannot be handed out twice by two calls. That single design choice — claim, do not peek — removes most of the concurrency problem before any lock is added.

Size fitting, without an if-else ladder

A car should take a medium spot, and a large one if no medium is free. Encode the ordering in the enum rather than in branches:

Java
public enum SpotSize {    SMALL(1), MEDIUM(2), LARGE(3);    private final int rank;    SpotSize(int rank) { this.rank = rank; }    public boolean fits(VehicleSize v) { return this.rank >= v.rank(); }    public static List<SpotSize> fittingSizes(VehicleSize v) {   // smallest first        return Arrays.stream(values()).filter(s -> s.fits(v)).toList();    }}

Now the allocation reads as intent:

Java
public class SpotAllocator {    private final List<Floor> floors;    public ParkingSpot allocate(VehicleSize size) {        for (SpotSize candidate : SpotSize.fittingSizes(size)) {   // smallest fitting first            for (Floor floor : floors) {                Optional<ParkingSpot> spot = floor.claimFree(candidate);                if (spot.isPresent()) return spot.get();            }        }        throw new NoSpotAvailableException(size);    }}

Loop order matters and is worth saying out loud: size outer, floor inner means a car takes a medium spot on floor five before a large spot on floor one, preserving large spots for trucks. Reversing the loops fills the lot floor by floor and strands trucks. That is a one-line policy decision with a real operational consequence, and naming it is a scoring moment.

The park and unpark flow

Java
public Ticket park(Vehicle vehicle) {    ParkingSpot spot = allocator.allocate(vehicle.getSize());    spot.assign(vehicle);    Ticket ticket = new Ticket(idGenerator.next(), vehicle, spot, clock.now());    activeTickets.put(ticket.getId(), ticket);    return ticket;}public Money unpark(String ticketId) {    Ticket ticket = activeTickets.remove(ticketId);    if (ticket == null) throw new UnknownTicketException(ticketId);    ticket.markExit(clock.now());    Money fee = pricingStrategy.computeFee(ticket);    ticket.getSpot().release();    ticket.getSpot().getFloor().release(ticket.getSpot());    return fee;}

Note clock is injected rather than System.currentTimeMillis() — the tip from Phase 5: coding the core, and what to skip, and the thing that makes fee calculation testable without waiting an hour.

Patterns applied

Two patterns belong in this design. Both are justified against writing an if-else, honestly — including the case where the if-else would have been fine.

Two patterns in, three patterns outEarns its place• Strategy: hourly, flat, weekend pricing• Factory: build spot and vehicle types• Both beat an if-else that keeps growingLeft out, and say why• Singleton for the lot— a global in disguise• Observer for a single display board• Builder for a four-field Ticket
Naming the pattern you declined scores as well as naming the one you used.

Strategy, for pricing

The mess it removes. Without it, the fee calculation lives inside ParkingLot or Ticket and grows a branch per rule:

Java
Money computeFee(Ticket t) {    long minutes = t.durationMinutes();    if (minutes <= 30) return Money.rupees(20);                       // flat first half-hour    if (isWeekend(t.getEntryTime())) return Money.rupees(150);        // weekend flat rate    if (t.hasMonthlyPass()) return Money.ZERO;                        // pass holders    long hours = (long) Math.ceil(minutes / 60.0);    if (t.getSpot().getSize() == SMALL)  return Money.rupees(20 * hours);    if (t.getSpot().getSize() == MEDIUM) return Money.rupees(50 * hours);    return Money.rupees(100 * hours);}

Four rules, one method, and each new rule is an edit to a method that already handles money. hasMonthlyPass() has also leaked a subscription concept onto Ticket.

The repair.

Java
public interface PricingStrategy { Money computeFee(Ticket ticket); }public class HourlyPricing implements PricingStrategy {    private final Map<SpotSize, Money> ratePerHour;    private final Money firstHalfHourFlat;    public Money computeFee(Ticket ticket) {        long minutes = ticket.durationMinutes();        if (minutes <= 30) return firstHalfHourFlat;        long hours = (long) Math.ceil(minutes / 60.0);        return ratePerHour.get(ticket.getSpot().getSize()).times(hours);    }}// WeekendFlatPricing, MonthlyPassPricing implement the same one method.

The justification, said out loud: "Pricing is the thing most likely to change in this system. The lot's structure will not change for years; the rate card changes every quarter and promotions change monthly. So pricing goes behind an interface and a new rule is a new class rather than an edit to a method that already handles money."

That sentence is the point of the pattern. The classes are the easy part.

Factory, for creating vehicles and spots

The mess it removes. Vehicle creation happens at every entry gate, and the mapping from an input type to a class is duplicated at each one.

Java
public final class VehicleFactory {    public static Vehicle create(VehicleType type, String plate) {        return switch (type) {            case MOTORCYCLE -> new Motorcycle(plate);            case CAR        -> new Car(plate);            case TRUCK      -> new Truck(plate);        };    }}

The honest justification. A static factory method is worth it here because gates are plural: four entry gates each turning a scanned vehicle type into an object. Without it, adding a van type means finding four switch statements.

And the honest limit. This is a static factory method, not an Abstract Factory. There is no family of related products, no second factory implementation, and no runtime choice of factory. Say so: "A static method is enough here — an abstract factory would add a class and buy nothing, because there is only one product family." That sentence, from When not to use a pattern, earns the same credit as using the pattern.

The patterns to leave out

Pattern candidates reach forWhy not here
Singleton for ParkingLotGlobal mutable state; makes two lots or two tests impossible. Create one at startup and pass it in
Observer on spot occupancyNothing needs to react. Add it if a display board becomes a requirement
State on ParkingSpotTwo states (free, occupied) with no per-state behaviour. A boolean is correct
Builder for TicketFour constructor arguments, all required. A constructor is correct

Saying "no State here — the spot has two states and no behaviour that differs between them, so a boolean is honest" is a stronger answer than adding two state classes.

Extensions

The last five minutes. The interviewer asks one or two of these, and your answer is graded on whether the model absorbs the change or has to be rebuilt.

The last five minutesDoes themodel absorb it?EV charging spotsReservationsMultiple lotsConcurrent assignment
Raise concurrency yourself: two cars at two gates racing for the last spot is the interesting one.

1. Electric-vehicle charging spots

The trap is ElectricCar extends Car. That is a second dimension of variation (Inheritance, and its limits) — an electric car is a car and needs charging — and inheritance can only express one.

The clean answer:

Java
public class ParkingSpot {    private final SpotSize size;    private final Set<SpotFeature> features;   // CHARGING, COVERED, ACCESSIBLE, VALET}public class Vehicle {    private final Set<SpotFeature> required;   // empty for most vehicles}

Allocation gains one filter: a spot must fit the size and carry every required feature. This handles covered parking, accessible bays, and valet spots with no further classes — which is the sign you picked the right axis.

2. Reservations

A reservation is a claim on a spot for a future window, so claimFree must not hand out a spot that is reserved for the time you would occupy it. Two honest options:

  • Reserve a specific spot. Simple, and wasteful — the spot sits empty until the holder arrives, and if they never do you have lost it.
  • Reserve a size, not a spot. Keep a count of reserved spots per size per window, and let claimFree refuse to drop free capacity below that count. More code, far better occupancy.

Recommend the second, and say why: a lot's economics are occupancy, and holding named bays empty is how you lose 5–10% of them on a busy day.

3. Multiple lots

Say plainly that this crosses into distributed system design. Within this model, extract a ParkingLotService that holds many lots and routes by location; the lot's internals are unchanged. The moment lots live on different machines, availability becomes a shared-state problem and the answer is a database or a coordination service, not a class.

4. The concurrency question — raise it yourself

This problem always ends here. Raise it before the interviewer does; candidates who do read as senior.

"One thing I want to name before we finish: with four entry gates, two threads can reach the last medium spot at the same moment. Let me walk through how I'd prevent that."

Three options, honestly compared:

A. One lock over the whole lot. Correct and trivially explainable. Every entry serialises, which at a few cars a minute is completely fine. Cost: unnecessary contention, and it blocks unrelated floors.

B. A lock per floor per size — lock only the queue you are polling.

Java
public Optional<ParkingSpot> claimFree(SpotSize size) {    Deque<ParkingSpot> queue = free.get(size);    synchronized (queue) {        return Optional.ofNullable(queue.pollFirst());    }}

Two gates looking for different sizes, or on different floors, never block each other.

C. A concurrent queue and no explicit lock. Use ConcurrentLinkedDeque and rely on pollFirst() being atomic — one caller gets the spot, the other gets null and tries the next floor. No lock at all.

Recommendation: C for the claim, with option A around the ticket-plus-spot update if the two must be consistent. The reasoning to say out loud: the atomic poll is exactly the operation we need, contention is low, and the claim-do-not-peek design from the spot-assignment code above already removed the check-then-act race. Reach for a coarse lock only if more than one thing must change together.