Problem 1: Design a Parking Lot
The setup
"Design the software for a parking lot. The parking lot has multiple floors, multiple spot types (motorcycle, compact, large, EV-charging), and processes ticketed entry/exit with payment. Support multiple payment methods."
This is the canonical LLD interview problem. It tests fundamentals: identifying entities, defining relationships, choosing the right abstractions, and applying the Strategy/Factory patterns. Almost every senior LLD interview at Indian tech companies uses some version of this.
Clarifying questions
| Question | Story answer |
|---|---|
| How many floors? Spots per floor? | 10 floors, ~200 spots per floor. |
| Vehicle types? | Motorcycle, Compact (car), Large (SUV/truck), EV-charging spot. |
| How is pricing calculated? | Per hour, varies by vehicle type. Different rate for EV (parking + charging cost). |
| Payment methods? | Cash, Credit Card, UPI. Must be extensible (add new methods later). |
| What happens if lot is full? | Reject entry with a "lot full" message. Optional: maintain a wait queue. |
| Concurrent access? | Yes — many cars may arrive simultaneously. Must handle correctly. |
| Real-time spot availability dashboard? | Out of scope; mention as an extension. |
Functional and non-functional requirements
Functional:
- Issue a ticket when a vehicle enters; assign an appropriate spot
- Calculate fee based on duration + vehicle type when vehicle exits
- Process payment via chosen method
- Free up spot when vehicle exits
- Handle "lot full" gracefully
Non-functional:
- Thread-safe: multiple entries/exits concurrently
- Extensible: easy to add new vehicle types, spot types, payment methods
- Auditable: every transaction logged
Core entities
| Entity | Responsibility |
|---|---|
| ParkingLot | Aggregates floors; entry point for park/exit operations |
| ParkingFloor | Holds spots for one floor; tracks availability per spot type |
| ParkingSpot | Represents a single spot; tracks its vehicle (if any), type, location |
| Vehicle | Represents the entering vehicle; has type, license plate |
| Ticket | Issued at entry; contains spot info, entry time, vehicle ref |
| FeeCalculator | Computes fee for a ticket (encapsulates pricing logic) |
| PaymentMethod | Interface; CreditCardPayment, UpiPayment, CashPayment implement it |
| PaymentProcessor | Takes a PaymentMethod and amount; handles processing |
| SpotAllocator | Strategy for finding the right spot for a vehicle |
Class hierarchy
classDiagram
direction LR
class ParkingLot {
+parkVehicle()
+exitVehicle()
}
class Floor {
+findSpot()
}
class Spot {
+assign()
+release()
}
class Vehicle
class Car
class Truck
class Motorcycle
class Ticket
class FeeStrategy
class PaymentMethod
ParkingLot *-- Floor
Floor *-- Spot
Spot -- Vehicle
Car ..|> Vehicle
Truck ..|> Vehicle
Motorcycle ..|> Vehicle
ParkingLot -- Ticket
ParkingLot -- FeeStrategy
ParkingLot -- PaymentMethod
// Vehicle hierarchy
abstract class Vehicle {
String licensePlate;
VehicleType getType(); // MOTORCYCLE | COMPACT | LARGE | EV
}
class Motorcycle extends Vehicle { ... }
class Car extends Vehicle { ... }
class Truck extends Vehicle { ... }
class ElectricCar extends Vehicle { ... }
// Spot hierarchy
abstract class ParkingSpot {
int spotId;
SpotType type;
Vehicle currentVehicle; // null if free
boolean canFit(Vehicle v);
void park(Vehicle v);
void release();
}
class MotorcycleSpot extends ParkingSpot { ... }
class CompactSpot extends ParkingSpot { ... }
class LargeSpot extends ParkingSpot { ... }
class EVChargingSpot extends ParkingSpot { ... }
Design Decision 1: Spot allocation strategy
How do we pick which spot to give an arriving vehicle? Multiple sub-decisions:
- A motorcycle can fit in a motorcycle spot, a compact spot, or a large spot. Prefer smallest fitting spot — leave larger spots for vehicles that need them.
- An electric car: prefer EV spot (has charger); fall back to compact/large if EV spots are full.
- Truck: must use Large spot.
This is a strategy. Use the Strategy pattern:
interface SpotAllocator {
ParkingSpot findSpot(Vehicle vehicle, ParkingLot lot);
}
class SmallestFitFirstAllocator implements SpotAllocator {
public ParkingSpot findSpot(Vehicle v, ParkingLot lot) {
// Try spot types in order: matching size, then larger
List preferredTypes = getPreferredTypes(v.getType());
for (SpotType type : preferredTypes) {
ParkingSpot spot = lot.findFreeSpot(type);
if (spot != null) return spot;
}
return null;
}
private List getPreferredTypes(VehicleType v) {
return switch (v) {
case MOTORCYCLE -> List.of(MOTORCYCLE_SPOT, COMPACT_SPOT, LARGE_SPOT);
case COMPACT -> List.of(COMPACT_SPOT, LARGE_SPOT);
case LARGE -> List.of(LARGE_SPOT);
case EV -> List.of(EV_SPOT, COMPACT_SPOT, LARGE_SPOT);
};
}
}
Why Strategy: future requirements might want different allocation policies — closest spot, nearest exit, premium spots — and we can swap by configuration.
Design Decision 2: Payment methods
Multiple payment methods, each with different processing logic. Strategy pattern again — payment methods are interchangeable strategies.
interface PaymentMethod {
PaymentResult pay(BigDecimal amount);
}
class CreditCardPayment implements PaymentMethod {
String cardNumber, cvv, expiry;
public PaymentResult pay(BigDecimal amount) {
// Call card processor; return result
}
}
class UpiPayment implements PaymentMethod {
String upiId;
public PaymentResult pay(BigDecimal amount) {
// Initiate UPI transaction
}
}
class CashPayment implements PaymentMethod {
public PaymentResult pay(BigDecimal amount) {
// Mark as cash received
}
}
The PaymentProcessor accepts any PaymentMethod:
class PaymentProcessor {
public PaymentResult process(PaymentMethod method, BigDecimal amount) {
PaymentResult result = method.pay(amount);
// log; emit event; etc.
return result;
}
}
Adding a new method (BitcoinPayment, ApplePay) is a new class implementing the interface — no modification to existing code. This is Open-Closed in action.
Design Decision 3: Fee calculation
Fees depend on vehicle type and duration. The simple approach is enough for v1:
class FeeCalculator {
private static final Map hourlyRates = Map.of(
MOTORCYCLE, new BigDecimal("10"),
COMPACT, new BigDecimal("30"),
LARGE, new BigDecimal("50"),
EV, new BigDecimal("40") // parking only; charging billed separately
);
public BigDecimal calculate(Ticket ticket, Instant exitTime) {
Duration duration = Duration.between(ticket.entryTime, exitTime);
long hours = (long) Math.ceil(duration.toMinutes() / 60.0); // round up
BigDecimal rate = hourlyRates.get(ticket.vehicleType);
return rate.multiply(BigDecimal.valueOf(hours));
}
}
For more complex pricing (time-of-day pricing, weekly subscription, VIP rates), introduce a PricingStrategy interface. Don't over-engineer for v1.
Design Decision 4: Concurrency
Multiple vehicles can arrive simultaneously. Two cars trying to take the last compact spot must result in exactly one parking, the other rejected.
class ParkingFloor {
private final Map> freeSpots;
private final Lock lock = new ReentrantLock();
public ParkingSpot acquireSpot(SpotType type) {
lock.lock();
try {
Queue queue = freeSpots.get(type);
return queue != null ? queue.poll() : null;
} finally {
lock.unlock();
}
}
public void releaseSpot(ParkingSpot spot) {
lock.lock();
try {
freeSpots.get(spot.type).offer(spot);
} finally {
lock.unlock();
}
}
}
The lock is per-floor: parallel parking on different floors doesn't block. For very high concurrency, consider lock-free structures (ConcurrentLinkedQueue), but for a 10-floor parking lot, simple locking is fine.
The main ParkingLot facade
class ParkingLot {
private final List floors;
private final SpotAllocator allocator;
private final FeeCalculator feeCalculator;
private final PaymentProcessor paymentProcessor;
private final TicketIssuer ticketIssuer;
public Ticket parkVehicle(Vehicle vehicle) {
ParkingSpot spot = allocator.findSpot(vehicle, this);
if (spot == null) throw new LotFullException();
spot.park(vehicle);
Ticket ticket = ticketIssuer.issue(vehicle, spot);
return ticket;
}
public Receipt exitVehicle(Ticket ticket, PaymentMethod paymentMethod) {
BigDecimal fee = feeCalculator.calculate(ticket, Instant.now());
PaymentResult result = paymentProcessor.process(paymentMethod, fee);
if (!result.isSuccess()) throw new PaymentFailedException();
ticket.spot.release();
return new Receipt(ticket, fee, result.getTransactionId());
}
ParkingSpot findFreeSpot(SpotType type) {
for (ParkingFloor floor : floors) {
ParkingSpot spot = floor.acquireSpot(type);
if (spot != null) return spot;
}
return null;
}
}
Cross-examination round 1: extensibility
A: Refactor FeeCalculator into a PricingStrategy interface with implementations FlatRateStrategy, TimeOfDayStrategy, etc. Inject the strategy at construction. Now we can per-vehicle assign different strategies if needed.
Cross-examination round 2: testing
Cross-examination round 3: edge cases
Takeaway
The parking lot is canonical because it hits every LLD primitive: entity identification (Vehicle, Spot, Ticket), inheritance (vehicle/spot types), Strategy pattern (allocator, payment, pricing), Open-Closed (extensible without modifying existing code), and concurrency (thread-safe spot allocation). A senior candidate's design defends each decision with reasoning — why Strategy here, why not just an enum-switch — and demonstrates that they could extend the design tomorrow without breaking existing code.
The deeper lesson: good LLD is about choosing the right level of abstraction for each piece. Vehicle types are an inheritance hierarchy because they have intrinsic behavior differences. Payment methods are a strategy interface because they're interchangeable algorithms. Spots are objects-with-state because they have a lifecycle (free → occupied → free). Each choice matches the problem.