Skip to content
Jarviix
LLD9 min read

Parking Lot

Multi-floor parking with size-based slot allocation, ticket lifecycle and pricing strategy.

lldoopdesign

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

Skeleton — interfaces + park flowjava
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.

Related reads