Parking Lot
Multi-floor parking with size-based slot allocation, ticket lifecycle and pricing strategy.
Intro
A parking lot has floors, and each floor has slots of different sizes (small / medium / large). Vehicles arrive, take a ticket, occupy a slot, and pay on exit. The interesting design questions are: where to put allocation logic, how to price flexibly, and what happens on contention.
Functional
- Park a vehicle of a given size; reject if no slot available.
- Issue a ticket on entry; capture exit time and amount on exit.
- Support multiple floors and slot sizes.
- Pricing: per-hour, with a minimum charge and a daily cap.
Non-functional
- Single instance per physical lot — concurrency safety on slot allocation.
- Pluggable pricing strategy without touching the booking flow.
- Auditable: every park/unpark must be reconstructible.
Components
ParkingLot
Aggregate root. Owns floors and exposes park/unpark.
ParkingFloor
Holds slots; knows occupancy by size.
ParkingSlot
Single addressable spot. Has size + occupant ticket id.
Ticket
Identity for an active parking event. Stores entry time + slot id.
PricingStrategy
Interface — implementations: hourly, daily-capped, weekend-pass.
SlotAllocator
Picks the next free slot. Default = nearest empty of required size.
PaymentGateway
Charges card/UPI; idempotent on ticket id.
Trade-offs
Allocation: nearest-slot vs. round-robin
Pros
- Nearest reduces walk distance, matches user expectation.
- Round-robin balances wear on barriers.
Cons
- Nearest = O(log slots) with a sorted set; round-robin = O(1).
Concurrency: per-slot lock vs. per-floor lock
Pros
- Per-slot scales when many entries arrive at once.
- Per-floor is much simpler to reason about.
Cons
- Per-slot multiplies lock objects; per-floor serialises within a floor.
Code
enum SlotSize { SMALL, MEDIUM, LARGE }
class ParkingSlot {
final String id; final SlotSize size; volatile String ticketId;
ParkingSlot(String id, SlotSize size) { this.id = id; this.size = size; }
synchronized boolean tryAssign(String ticketId) {
if (this.ticketId != null) return false;
this.ticketId = ticketId; return true;
}
synchronized void release() { this.ticketId = null; }
}
interface PricingStrategy { long compute(Instant in, Instant out); }
class ParkingLot {
final List<ParkingFloor> floors;
final SlotAllocator allocator;
final PricingStrategy pricing;
final TicketRepository tickets;
Ticket park(Vehicle v) {
ParkingSlot slot = allocator.pick(floors, sizeFor(v));
if (slot == null) throw new LotFullException();
Ticket t = Ticket.issue(slot.id, Instant.now());
if (!slot.tryAssign(t.id)) return park(v);
tickets.save(t);
return t;
}
Receipt unpark(String ticketId) {
Ticket t = tickets.findOpen(ticketId);
long amount = pricing.compute(t.entry, Instant.now());
slot(t.slotId).release();
return Receipt.paid(t, amount);
}
}Pitfalls
- Putting pricing inside the slot or vehicle — kills extensibility.
- Using a global lock around park() — fine for a small lot, will not scale to airport-sized lots.
- Forgetting that the same ticket id may be presented twice — payment must be idempotent.