Design a parking lot system — you have 90 minutes, working code at the end.
Scope it in the first five minutes to one deterministic flow of park, unpark and charge, model Vehicle, Slot, Level, Ticket and a ParkingLot facade, then push slot allocation and pricing behind strategy interfaces so the variants asked at review time are configuration rather than surgery.
What the interviewer is scoring
- Whether you fix the scope out loud before writing a line, rather than discovering at minute 70 that you have no working flow
- Whether your interfaces separate policy from mechanism, so a new pricing rule touches one class and no others
- Whether the code you hand over actually runs end to end, with a driver or a test proving park then unpark then charge
- Whether you name the concurrency assumption explicitly instead of leaving a silent race in slot allocation
- Whether you resist modelling entities the stated requirements never mention, such as reservations, payments gateways or users
Answer
The first five minutes decide the outcome
A machine-coding round is not a design round with a compiler attached. It is graded on running code, and the single most common failure is a candidate who spends forty minutes on a class diagram and hands over something that does not compile. So the first thing you do is narrow the problem out loud and get agreement.
State the flow you will build: a vehicle arrives, the system finds a free slot that fits it, issues a ticket, and on exit computes a fee from the duration and marks the slot free. State what you are deliberately excluding: payment gateways, reservations, authentication, persistence, a REST layer, and multi-lot federation. Then state the two axes you expect to vary, because those are the ones you will make pluggable: how a slot is chosen, and how a fee is computed. Ask one clarifying question that actually changes the model — usually "can a small vehicle occupy a large slot?" — because the answer determines whether slot fitting is an equality check or an ordering.
That five-minute contract is worth spending time on. It converts an open-ended problem into a bounded one, and it gives the interviewer an explicit list they can hold you to instead of silently marking you down for gaps you never acknowledged.
The core model
Keep entities dumb and behaviour thin. A Slot knows its size and whether it is occupied; it does not know about pricing. A Ticket is an immutable record of what entered, where, and when. ParkingLot is the facade the driver code talks to, and it is the only class that mutates state.
public enum VehicleType { MOTORCYCLE, CAR, TRUCK }
public record Vehicle(String registrationNumber, VehicleType type) {}
public final class Slot {
private final String id;
private final VehicleType size;
private Vehicle occupant; // null means free
public Slot(String id, VehicleType size) {
this.id = id;
this.size = size;
}
public boolean canFit(VehicleType t) {
// Ordinal ordering only works because the enum is declared
// smallest-first. Say this out loud; it is a real coupling.
return occupant == null && size.ordinal() >= t.ordinal();
}
public String id() { return id; }
public void assign(Vehicle v) { this.occupant = v; }
public void release() { this.occupant = null; }
}
public record Ticket(String id, Vehicle vehicle, String slotId, Instant entryTime) {}
A Level is a named collection of slots, and the lot holds an ordered list of levels. Resist adding a Floor, a Gate, an Attendant and a DisplayBoard unless the requirements named them. Every extra entity is code you must make work in 90 minutes.
Where the pluggable decisions go
Two interfaces carry the whole design's flexibility. Allocation decides which free slot to use; pricing decides what to charge. Neither should ever be an if chain inside ParkingLot.
public interface SlotAllocationStrategy {
Optional<Slot> allocate(List<Level> levels, Vehicle vehicle);
}
public interface PricingStrategy {
Money priceFor(Ticket ticket, Instant exitTime);
}
NearestFirstAllocation scans levels in order and returns the first fitting slot. LeastOccupiedLevelAllocation picks the emptiest level first to spread load. HourlyPricing rounds the duration up to whole hours and multiplies by a per-type rate; SlabPricing charges a flat amount for the first two hours and an increasing rate afterwards. The interviewer will almost certainly ask for the second variant of one of these, and the correct response is to write one new class and change one constructor argument.
public final class ParkingLot {
private final List<Level> levels;
private final SlotAllocationStrategy allocation;
private final PricingStrategy pricing;
private final Clock clock; // injected, so tests can fast-forward
private final Map<String, Ticket> active = new ConcurrentHashMap<>();
public synchronized Ticket park(Vehicle v) {
Slot slot = allocation.allocate(levels, v)
.orElseThrow(() -> new LotFullException(v.type()));
slot.assign(v); // find-then-assign must be atomic
Ticket t = new Ticket(UUID.randomUUID().toString(), v, slot.id(), clock.instant());
active.put(t.id(), t);
return t;
}
// Synchronised for the same reason as park: unpark calls slot.release(),
// so leaving it unguarded reopens the check-then-act hole park just closed.
public synchronized Receipt unpark(String ticketId, Instant exitTime) { /* release + price */ }
}
Injecting a Clock rather than calling Instant.now() inside the class is a small move that pays for itself immediately: it is the only way to unit-test a three-hour parking session in a 90-minute round.
The concurrency assumption
Allocation is a check-then-act sequence, so two threads can both see the same slot as free. You do not need a sophisticated answer, but you do need a stated one. The cheap, defensible choice is to synchronise park and unpark on the lot, acknowledge that this serialises all entries, and name the refinement you would make with more time: a per-level lock, or an available-slot queue per vehicle type so allocation becomes an atomic poll() instead of a scan. An unmentioned race reads as an oversight; a named one with a stated upgrade path reads as judgement.
What reviewers actually grade
Almost none of the marks are for the class diagram. Reviewers open the repository and look for four things in order. First, does it run — is there a main or a test that parks a car, advances the clock, unparks it and prints a fee? A design that does not execute scores below an uglier one that does. Second, can they add a requirement cheaply: they will mentally insert electric-vehicle charging slots or weekend pricing and count how many files you would have to touch. Third, is the code readable without narration — meaningful names, no 200-line god method, exceptions instead of returning null. Fourth, is anything tested at all; two or three focused JUnit tests on pricing boundaries and the lot-full path outscore twenty entity classes with no test.
Budget accordingly. Roughly fifteen minutes on scoping and skeleton interfaces, fifty on making the happy path plus lot-full and pricing work, fifteen on tests, and ten held back for the walkthrough where you explain the trade-offs you consciously made.
The trap
The trap is over-modelling in the first half hour. Parking lot is famous, so candidates arrive with a remembered diagram containing gates, attendants, payment processors, display boards and an admin, and they start typing all of it. At minute 75 they have fifteen half-finished classes, nothing that runs, and no tests, and they lose to a candidate who shipped four classes and two strategies that work. The discipline is to build the thinnest vertical slice that executes end to end, then widen it — and to say explicitly, "I am leaving payments out and here is the interface I would add it behind," which earns the design credit without costing the implementation time.
Working code with two well-placed strategy interfaces beats a complete class diagram that does not compile; in a timed round, scope is the design decision you are being graded on.
Likely follow-ups
- Two cars request the last available slot at the same instant — where exactly is your lock, and what is its granularity?
- How would you support a monthly pass holder who parks free, without adding an if-branch to the pricing code?
- The lot now spans ten levels and slot lookup is the bottleneck — what data structure replaces your linear scan?
- How would you persist and rebuild lot state after a restart without leaking JPA annotations into your domain model?
Related questions
- Go gives you no project layout and no dependency-injection container. How do you structure a service so it stays testable as it grows?mediumAlso on interfaces5 min
- Design a rate limiter that allows N requests per client per minute. Write the class.mediumAlso on machine-coding4 min
- Your function returns nil on the success path, the caller checks err != nil, and the check fires anyway. What happened?hardAlso on interfaces4 min
- When would you use the strategy pattern instead of inheritance?mediumAlso on strategy-pattern5 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: design7 min
- Design the data structure behind a search box's autocomplete. Why a trie, and when would you not use one?mediumSame kind of round: design6 min
- Design a producer-consumer pipeline with a bounded buffer. What do you do when the buffer is full?hardSame kind of round: design5 min
- You run ten coroutines concurrently and one of them raises. What happens to the other nine?hardSame kind of round: coding5 min