Parking Lot
Write it yourself first (45–60 minutes, plain editor, no autocomplete). Use any language. Aim for code that runs, with a tiny
mainthat parks and un-parks a few vehicles. Only then use the checklist below to review your own work.
The problem
Design the software for a multi-floor parking lot.
Requirements
- The lot has several floors, each with spots of different sizes: bike, compact (car), large (truck/bus).
- A vehicle gets a ticket at the entry gate with an assigned spot that fits its size.
- At the exit, the system computes the fee from the time parked and the vehicle type, takes payment, and frees the spot.
- Show availability per floor and per spot type.
- When the lot is full for a vehicle type, entry is refused with a clear message.
Follow-ups an interviewer may add mid-round (design so these are easy):
- A different pricing model (flat first hour, then hourly; weekend rates).
- Electric-vehicle spots with charging.
- Multiple entry and exit gates working at the same time.
Clarifying questions worth asking
- Can a small vehicle use a bigger spot if its own size is full?
- Is pricing per hour, rounded up? Different per vehicle type?
- Do we need persistence, or is in-memory fine for this round?
Self-review checklist
Tick these against your own code, honestly:
| ✔ | Check |
|---|---|
| ☐ | Core entities are separate classes: lot, floor, spot, vehicle, ticket |
| ☐ | Spot allocation is its own strategy, so "nearest spot" can become "cheapest floor" without touching other classes |
| ☐ | Pricing is its own strategy (an interface with implementations), not an if/else inside the exit gate |
| ☐ | Adding a new vehicle or spot type does not mean editing many switch statements |
| ☐ | Two gates parking at once cannot get the same spot (what do you lock, and how little?) |
| ☐ | Errors are explicit: lot full, invalid ticket, ticket already paid |
| ☐ | A main or test actually runs the flow end to end |
After you have written it
Note down what took longest and which checklist items you missed. Those are what to practise before the next round.
⏱ Interview mode
90-minute machine coding round90:00- 1. Clarify and assumptions5 min
- 2. Entities and interfaces10 min
- 3. Core code (make it run)50 min
- 4. Demo, tests, errors15 min
- 5. Extensions and concurrency10 min
Show solutionTried it yourself first? Open it to compare and note what you missed.
Assumptions (say these out loud first)
- A smaller vehicle may use a bigger spot when its own size is full; never the reverse.
- Pricing is per started hour, by vehicle type, minimum one hour.
- In-memory is fine for this round. Persistence is a follow-up.
- Several gates can park and exit at the same time.
Design
| Class | Responsibility |
|---|---|
VehicleType, SpotSize | Enums. VehicleType.fits(size) holds the "small can use big" rule in one place |
Vehicle, Ticket, Receipt | Immutable records: plain data |
Spot | Knows its size and floor, and can be claimed atomically (tryTake) |
Floor | Holds its spots |
SpotAllocator (interface) | Decides which spot. NearestSmallestFit is one implementation |
PricingPolicy (interface) | Decides how much. HourlyPricing is one implementation |
ParkingLot | The service the gates call: park, exit, availability |
ParkingLot ──uses──▶ SpotAllocator (NearestSmallestFit, …)
│ ──uses──▶ PricingPolicy (HourlyPricing, WeekendPricing, …)
│ ──uses──▶ Clock (real clock, or a test clock)
└──has──▶ Floor ──has──▶ Spot
Ticket ──▶ Vehicle, Spot
Why these choices
- Strategy pattern for allocation and pricing. These are exactly what interviewers change mid-round ("now add weekend pricing", "fill the top floor first"). With an interface, a change is one new class, and nothing else is edited.
- The fit rule lives in the enum. Adding a vehicle type means one enum line, not hunting through
switchstatements. Clockis injected. Fees depend on time; injecting the clock makes the logic testable without waiting two hours.- Records for data. Tickets and receipts never change after creation, which removes a class of bugs.
Concurrency: two gates, one spot
Each spot has an AtomicBoolean. The only way to claim it is compareAndSet(false, true). If two gates pick the same free spot, exactly one wins; the loser just moves to the next spot. No global lock, so gates never wait for each other.
Exit uses ConcurrentHashMap.remove(ticketId), which is atomic, so the same ticket cannot be paid twice.
The code
Java 21, one file. Run it with java ParkingLot.java.
// Parking Lot: reference solution for the CrackLoop LLD chapter.
// Run with Java 21: java ParkingLot.java
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public class ParkingLot {
// ---- Domain ---------------------------------------------------------------------------
enum SpotSize { SMALL, MEDIUM, LARGE }
enum VehicleType {
BIKE(SpotSize.SMALL), CAR(SpotSize.MEDIUM), TRUCK(SpotSize.LARGE);
final SpotSize needs;
VehicleType(SpotSize needs) { this.needs = needs; }
boolean fits(SpotSize s) { return s.ordinal() >= needs.ordinal(); } // a bike may use a car spot, not the reverse
}
record Vehicle(String plate, VehicleType type) {}
static final class Spot {
final String id; final int floor; final SpotSize size;
private final AtomicBoolean taken = new AtomicBoolean(false);
Spot(String id, int floor, SpotSize size) { this.id = id; this.floor = floor; this.size = size; }
boolean tryTake() { return taken.compareAndSet(false, true); } // the only way to claim a spot: lock-free, race-safe
void release() { taken.set(false); }
boolean isFree() { return !taken.get(); }
public String toString() { return id; }
}
static final class Floor {
final int number; final List<Spot> spots;
Floor(int number, int small, int medium, int large) {
this.number = number;
List<Spot> s = new ArrayList<>();
for (int i = 0; i < small; i++) s.add(new Spot("F" + number + "-S" + i, number, SpotSize.SMALL));
for (int i = 0; i < medium; i++) s.add(new Spot("F" + number + "-M" + i, number, SpotSize.MEDIUM));
for (int i = 0; i < large; i++) s.add(new Spot("F" + number + "-L" + i, number, SpotSize.LARGE));
this.spots = List.copyOf(s);
}
}
record Ticket(String id, Vehicle vehicle, Spot spot, Instant entry) {}
record Receipt(String ticketId, String plate, Duration parked, long feeRupees) {}
static class ParkingException extends RuntimeException { ParkingException(String m) { super(m); } }
// ---- Strategies: the parts interviewers ask you to change mid-round ------------------
interface SpotAllocator { Optional<Spot> claim(List<Floor> floors, VehicleType type); }
/** Lowest floor first, and on each floor the smallest spot that fits (keeps big spots for big vehicles). */
static final class NearestSmallestFit implements SpotAllocator {
public Optional<Spot> claim(List<Floor> floors, VehicleType type) {
for (Floor f : floors)
for (SpotSize size : SpotSize.values()) {
if (!type.fits(size)) continue;
for (Spot s : f.spots)
if (s.size == size && s.isFree() && s.tryTake()) return Optional.of(s); // lost a race? try the next one
}
return Optional.empty();
}
}
interface PricingPolicy { long fee(VehicleType type, Duration parked); }
/** Per-hour rate by vehicle type, every started hour billed, minimum one hour. */
static final class HourlyPricing implements PricingPolicy {
private final Map<VehicleType, Long> perHour;
HourlyPricing(Map<VehicleType, Long> perHour) { this.perHour = Map.copyOf(perHour); }
public long fee(VehicleType type, Duration parked) {
long minutes = Math.max(1, parked.toMinutes());
long hours = (minutes + 59) / 60;
return hours * perHour.get(type);
}
}
// ---- The lot ----------------------------------------------------------------------------
private final List<Floor> floors;
private final SpotAllocator allocator;
private final PricingPolicy pricing;
private final Clock clock;
private final Map<String, Ticket> active = new ConcurrentHashMap<>();
private final AtomicLong ticketSeq = new AtomicLong();
ParkingLot(List<Floor> floors, SpotAllocator allocator, PricingPolicy pricing, Clock clock) {
this.floors = List.copyOf(floors); this.allocator = allocator; this.pricing = pricing; this.clock = clock;
}
/** Entry gate. Safe to call from many gates at once. */
Ticket park(Vehicle v) {
Spot spot = allocator.claim(floors, v.type())
.orElseThrow(() -> new ParkingException("Lot full for " + v.type()));
Ticket t = new Ticket("T" + ticketSeq.incrementAndGet(), v, spot, clock.instant());
active.put(t.id(), t);
return t;
}
/** Exit gate: fee, then the spot is free again. A ticket can only be used once. */
Receipt exit(String ticketId) {
Ticket t = active.remove(ticketId); // atomic: two exits with the same ticket can't both succeed
if (t == null) throw new ParkingException("Unknown or already used ticket " + ticketId);
Duration parked = Duration.between(t.entry(), clock.instant());
long fee = pricing.fee(t.vehicle().type(), parked);
t.spot().release();
return new Receipt(t.id(), t.vehicle().plate(), parked, fee);
}
/** Free spots per floor and size, e.g. {1={SMALL=2, MEDIUM=1, LARGE=0}}. */
Map<Integer, Map<SpotSize, Long>> availability() {
Map<Integer, Map<SpotSize, Long>> out = new TreeMap<>();
for (Floor f : floors) {
Map<SpotSize, Long> m = new EnumMap<>(SpotSize.class);
for (SpotSize s : SpotSize.values()) m.put(s, f.spots.stream().filter(x -> x.size == s && x.isFree()).count());
out.put(f.number, m);
}
return out;
}
// ---- Demo ---------------------------------------------------------------------------------
/** A clock the demo can move forward. */
static final class TestClock extends Clock {
private Instant now = Instant.parse("2026-11-01T09:00:00Z");
void advance(Duration d) { now = now.plus(d); }
public Instant instant() { return now; }
public java.time.ZoneId getZone() { return ZoneOffset.UTC; }
public Clock withZone(java.time.ZoneId zone) { return this; }
}
public static void main(String[] args) throws Exception {
TestClock clock = new TestClock();
ParkingLot lot = new ParkingLot(
List.of(new Floor(1, 1, 1, 0), new Floor(2, 0, 2, 1)),
new NearestSmallestFit(),
new HourlyPricing(Map.of(VehicleType.BIKE, 10L, VehicleType.CAR, 40L, VehicleType.TRUCK, 100L)),
clock);
Ticket bike = lot.park(new Vehicle("DL-1AB-1111", VehicleType.BIKE));
Ticket car = lot.park(new Vehicle("DL-2CD-2222", VehicleType.CAR));
Ticket truck = lot.park(new Vehicle("HR-3EF-3333", VehicleType.TRUCK));
System.out.println("bike -> " + bike.spot() + ", car -> " + car.spot() + ", truck -> " + truck.spot());
System.out.println("free : " + lot.availability());
try { lot.park(new Vehicle("UP-4GH-4444", VehicleType.TRUCK)); }
catch (ParkingException e) { System.out.println("refused: " + e.getMessage()); }
clock.advance(Duration.ofMinutes(130)); // 2h10m → 3 started hours
System.out.println("exit : " + lot.exit(car.id()));
try { lot.exit(car.id()); }
catch (ParkingException e) { System.out.println("refused: " + e.getMessage()); }
// Two gates racing: 50 cars, 3 medium-or-larger spots left. Exactly 3 must get in, each in a different spot.
ExecutorService gates = Executors.newFixedThreadPool(8);
List<Future<Ticket>> tries = new ArrayList<>();
for (int i = 0; i < 50; i++) {
Vehicle v = new Vehicle("RACE-" + i, VehicleType.CAR);
tries.add(gates.submit(() -> { try { return lot.park(v); } catch (ParkingException e) { return null; } }));
}
Set<String> spots = new HashSet<>(); int admitted = 0;
for (Future<Ticket> f : tries) { Ticket t = f.get(); if (t != null) { admitted++; spots.add(t.spot().id); } }
gates.shutdown();
System.out.println("race : " + admitted + " admitted, " + spots.size() + " distinct spots");
}
}
What the demo prints
bike -> F1-S0, car -> F1-M0, truck -> F2-L0
free : {1={SMALL=0, MEDIUM=0, LARGE=0}, 2={SMALL=0, MEDIUM=2, LARGE=0}}
refused: Lot full for TRUCK
exit : Receipt[ticketId=T2, plate=DL-2CD-2222, parked=PT2H10M, feeRupees=120]
refused: Unknown or already used ticket T2
race : 3 admitted, 3 distinct spots
The last line is the important one: 50 cars race through 8 gates for 3 spots, and exactly 3 get in, each in a different spot.
Complexity and the obvious improvement
park scans spots: O(number of spots). Fine for a few hundred. For a large lot, keep a free list per (floor, size), for example a ConcurrentLinkedDeque<Spot>: park pops one in O(1), exit pushes it back. Mention this even if you don't code it.
Answering the follow-ups
| Follow-up | Change |
|---|---|
| Weekend or flat-first-hour pricing | New PricingPolicy class; pass it in. Nothing else changes |
| EV spots with charging | Add EV to SpotSize (or a charging flag on Spot) and a vehicle type that needs it; charge time can be a second policy |
| Fill the top floor first | New SpotAllocator |
| Multiple gates | Already safe: atomic spot claims and atomic ticket removal |
| Persistence | Put a TicketRepository interface in front of the active map; the claim becomes a conditional update in the database (UPDATE … WHERE taken = false) |
Writing it in Go or TypeScript
Same design: interfaces stay interfaces, enums become constants. In Go, claim a spot with atomic.Bool.CompareAndSwap and keep active tickets in a sync.Map. In TypeScript on Node, a single process has no threads, so the race comes from async gaps instead. Claim spots synchronously (no await between checking and taking), or use a database row lock once state is shared.
What interviewers look for
- Requirements and assumptions stated before code
- Clear entities, and the changeable parts behind interfaces
- Code that runs, with a small demo
- An answer for concurrency, and for "what if this gets big"