Loading content...
A design that handles the happy path is a proof of concept. A design that handles edge cases is a production system. In interviews, demonstrating awareness of edge cases signals senior-level thinking—you're not just solving the obvious problem, you're anticipating the thousand ways reality will test your assumptions.
This page covers two dimensions of design maturity:
For each, we'll explore the problem, design implications, and implementation approaches.
By the end of this page, you will be able to identify common edge cases in parking lot systems, design solutions that handle exceptional scenarios gracefully, extend the base design for real-world requirements, and demonstrate production-thinking in interviews by proactively discussing edge cases.
Real parking lots have multiple entry and exit gates operating simultaneously. This creates race conditions that can cause the same spot to be allocated to two vehicles.
The Race Condition:
Gate 1: findSpot() → returns Spot A (available) [Thread 1]
Gate 2: findSpot() → returns Spot A (still available) [Thread 2]
Gate 1: spot.park(car1) [SUCCESS]
Gate 2: spot.park(car2) [SHOULD FAIL but might not!]
If the check and update aren't atomic, both vehicles might think they've parked successfully.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
// ============================================// SOLUTION 1: Optimistic Locking// ============================================// Try to allocate, but verify at commit time class OptimisticParkingSpot implements IParkingSpot { private version: number = 0; // Version counter private status: SpotStatus = SpotStatus.AVAILABLE; private parkedVehicle: IVehicle | null = null; getVersion(): number { return this.version; } /** * Attempt to park with version check * Throws if spot was modified since we checked */ parkWithVersion(vehicle: IVehicle, expectedVersion: number): void { if (this.version !== expectedVersion) { throw new ConcurrentModificationError( `Spot ${this.id} was modified by another transaction` ); } if (!this.isAvailable()) { throw new SpotNotAvailableError(`Spot ${this.id} is occupied`); } this.parkedVehicle = vehicle; this.status = SpotStatus.OCCUPIED; this.version++; // Increment version on change }} // Allocation with retry logicclass OptimisticAllocationStrategy implements IAllocationStrategy { private readonly baseStrategy: IAllocationStrategy; private readonly maxRetries: number = 3; constructor(baseStrategy: IAllocationStrategy) { this.baseStrategy = baseStrategy; } findAndPark( vehicle: IVehicle, spotsByType: Map<SpotType, OptimisticParkingSpot[]> ): OptimisticParkingSpot { for (let attempt = 0; attempt < this.maxRetries; attempt++) { const spot = this.baseStrategy.findSpot( vehicle, spotsByType as Map<SpotType, IParkingSpot[]> ) as OptimisticParkingSpot; if (!spot) { throw new NoAvailableSpotError('No spots available'); } try { const version = spot.getVersion(); spot.parkWithVersion(vehicle, version); return spot; // Success! } catch (e) { if (e instanceof ConcurrentModificationError) { // Someone else got it, try again continue; } throw e; } } throw new AllocationFailedError( `Failed to allocate spot after ${this.maxRetries} attempts` ); }} // ============================================// SOLUTION 2: Pessimistic Locking (Mutex)// ============================================// Lock the spot before checking, guarantee exclusive access class LockedParkingSpot implements IParkingSpot { private lock = new Mutex(); // Thread-safe lock primitive private status: SpotStatus = SpotStatus.AVAILABLE; private parkedVehicle: IVehicle | null = null; async parkWithLock(vehicle: IVehicle): Promise<void> { // Acquire exclusive lock const release = await this.lock.acquire(); try { if (!this.isAvailable()) { throw new SpotNotAvailableError(`Spot ${this.id} is occupied`); } this.parkedVehicle = vehicle; this.status = SpotStatus.OCCUPIED; } finally { // Always release the lock release(); } }} // ============================================// SOLUTION 3: Lot-Level Atomic Allocation// ============================================// Synchronize at the ParkingLot level, not spot level class ThreadSafeParkingLot { private readonly allocationLock = new Mutex(); async parkVehicle(vehicle: IVehicle): Promise<Ticket> { const release = await this.allocationLock.acquire(); try { // Find spot - no one else can allocate while we hold the lock const spot = this.allocationStrategy.findSpot( vehicle, this.spotsByType ); if (!spot) { throw new NoAvailableSpotError('Lot is full'); } // Park and create ticket atomically spot.park(vehicle); const ticket = new Ticket( TicketIdGenerator.generate(), vehicle, spot, new Date() ); this.activeTickets.set(ticket.ticketId, ticket); return ticket; } finally { release(); } }} // Note: For high-throughput lots, lot-level locking creates a bottleneck// Choose based on expected concurrency:// - Low: Lot-level lock (simple)// - Medium: Optimistic locking (good throughput, occasional retries)// - High: Distributed locking with sharding by floor/sectionIn interviews, mention that you're aware of concurrency concerns and propose a strategy based on expected load. Don't implement full mutex logic—just explain: 'For a high-traffic lot with multiple gates, I'd use optimistic locking with retry logic to avoid the bottleneck of global locks while still preventing double-allocation.'
What happens when a driver loses their ticket? They still need to exit. The system must have a mechanism to handle this common scenario.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
// ============================================// LOST TICKET HANDLING// ============================================ // Special pricing for lost ticketsclass LostTicketPricingStrategy implements IPricingStrategy { private readonly maxDailyRate: number; private readonly flatPenalty: number; constructor(maxDailyRate: number = 50, flatPenalty: number = 10) { this.maxDailyRate = maxDailyRate; this.flatPenalty = flatPenalty; } calculateFee(ticket: ITicket): Money { // For lost tickets with unknown entry time, charge maximum daily rate // Plus a flat penalty return new Money(this.maxDailyRate + this.flatPenalty); } getDisplayName(): string { return 'Lost Ticket Rate'; } getDescription(): string { return 'Maximum daily rate plus penalty for unverifiable entry time'; }} // Entry point for lost ticket resolutioninterface ILostTicketService { reportLost(vehiclePlate: string): LostTicketReport; findByLicensePlate(plate: string): Ticket | null; processLostExit(plate: string): ExitResult;} class LostTicketService implements ILostTicketService { private readonly activeTickets: Map<string, Ticket>; private readonly ticketsByPlate: Map<string, Ticket>; // Secondary index private readonly lostTicketPricing: IPricingStrategy; constructor( activeTickets: Map<string, Ticket>, lostTicketPricing: IPricingStrategy ) { this.activeTickets = activeTickets; this.lostTicketPricing = lostTicketPricing; // Build secondary index this.ticketsByPlate = new Map(); for (const ticket of activeTickets.values()) { this.ticketsByPlate.set(ticket.vehicleLicensePlate, ticket); } } /** * Driver reports lost ticket * System attempts to find ticket by license plate */ reportLost(vehiclePlate: string): LostTicketReport { const ticket = this.findByLicensePlate(vehiclePlate); if (!ticket) { return { found: false, message: `No active ticket found for plate ${vehiclePlate}`, suggestedAction: 'Please see attendant with vehicle registration' }; } // Mark ticket as lost (for audit trail) ticket.markLost(); // Calculate what they would owe const regularFee = this.estimateRegularFee(ticket); const lostFee = this.lostTicketPricing.calculateFee(ticket); return { found: true, ticket, spotId: ticket.spotId, entryTime: ticket.entryTime, regularFee, lostFee, message: 'Ticket found. Lost ticket surcharge applies.' }; } findByLicensePlate(plate: string): Ticket | null { return this.ticketsByPlate.get(plate.toUpperCase()) || null; } processLostExit(plate: string): ExitResult { const report = this.reportLost(plate); if (!report.found || !report.ticket) { throw new CannotProcessExitError( 'Cannot locate vehicle record. Please see attendant.' ); } // Process with lost ticket pricing const fee = this.lostTicketPricing.calculateFee(report.ticket); return { ticket: report.ticket, fee, originalEntryTime: report.ticket.entryTime, pricingApplied: 'Lost Ticket Rate' }; } private estimateRegularFee(ticket: Ticket): Money { // Would need access to regular pricing strategy // Simplified here return new Money(ticket.getDurationHours() * 4); // Assume $4/hr }} // Integrate into ParkingLotclass ParkingLot { private lostTicketService: ILostTicketService; /** * Exit without ticket - uses license plate lookup */ exitWithoutTicket(licensePlate: string): Money { const result = this.lostTicketService.processLostExit(licensePlate); // Free the spot const spot = this.allSpots.get(result.ticket.spotId); if (spot) { spot.vacate(); } // Record the exit result.ticket.markExited(new Date(), result.fee.amount); this.activeTickets.delete(result.ticket.ticketId); return result.fee; }}License plate lookup for lost tickets raises a fraud vector: someone could claim a car is theirs when it's not. Production systems often require visual verification (attendant confirms plate matches vehicle being driven out) or link to camera systems for validation.
What if a bus needs multiple large spots? What about vehicles that don't fit our type categories? Handling unusual cases gracefully is essential.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
// ============================================// MULTI-SPOT VEHICLE HANDLING// ============================================ // Extended vehicle interface for multi-spot vehiclesinterface IMultiSpotVehicle extends IVehicle { readonly requiredSpots: number; readonly mustBeAdjacent: boolean;} class Bus implements IMultiSpotVehicle { readonly licensePlate: string; readonly type = VehicleType.BUS; readonly requiredSpots: number; readonly mustBeAdjacent: boolean; constructor(licensePlate: string, length: 'standard' | 'articulated') { this.licensePlate = licensePlate; this.requiredSpots = length === 'standard' ? 1 : 2; this.mustBeAdjacent = true; } getCompatibleSpotTypes(): SpotType[] { return [SpotType.LARGE]; } getPreferredSpotType(): SpotType { return SpotType.LARGE; }} // Strategy that handles multi-spot allocationclass MultiSpotAllocationStrategy implements IAllocationStrategy { findSpot( vehicle: IVehicle, spotsByType: Map<SpotType, IParkingSpot[]> ): IParkingSpot | null { // Check if this is a multi-spot vehicle if (this.isMultiSpotVehicle(vehicle)) { return this.findAdjacentSpots(vehicle as IMultiSpotVehicle, spotsByType); } // Fall back to single spot allocation return this.findSingleSpot(vehicle, spotsByType); } private isMultiSpotVehicle(vehicle: IVehicle): vehicle is IMultiSpotVehicle { return 'requiredSpots' in vehicle && (vehicle as IMultiSpotVehicle).requiredSpots > 1; } private findAdjacentSpots( vehicle: IMultiSpotVehicle, spotsByType: Map<SpotType, IParkingSpot[]> ): IParkingSpot | null { const compatibleTypes = vehicle.getCompatibleSpotTypes(); const required = vehicle.requiredSpots; for (const spotType of compatibleTypes) { const spots = spotsByType.get(spotType) || []; // Group by floor and row const byLocation = this.groupSpotsByLocation(spots); // Find a run of adjacent available spots for (const [_location, locationSpots] of byLocation) { const adjacentRun = this.findAdjacentRun(locationSpots, required); if (adjacentRun) { // Return the first spot; caller will need to reserve all return adjacentRun[0]; } } } return null; } private groupSpotsByLocation( spots: IParkingSpot[] ): Map<string, IParkingSpot[]> { const groups = new Map<string, IParkingSpot[]>(); for (const spot of spots) { const key = `${spot.floor}-${spot.row}`; if (!groups.has(key)) { groups.set(key, []); } groups.get(key)!.push(spot); } // Sort each group by spot number for (const group of groups.values()) { group.sort((a, b) => a.spotNumber - b.spotNumber); } return groups; } private findAdjacentRun( spots: IParkingSpot[], required: number ): IParkingSpot[] | null { let currentRun: IParkingSpot[] = []; let lastNumber = -2; for (const spot of spots) { if (!spot.isAvailable()) { currentRun = []; lastNumber = -2; continue; } if (spot.spotNumber === lastNumber + 1) { // Adjacent to previous currentRun.push(spot); } else { // Start new run currentRun = [spot]; } lastNumber = spot.spotNumber; if (currentRun.length >= required) { return currentRun; } } return null; } private findSingleSpot( vehicle: IVehicle, spotsByType: Map<SpotType, IParkingSpot[]> ): IParkingSpot | null { // Standard single-spot logic for (const spotType of vehicle.getCompatibleSpotTypes()) { const spots = spotsByType.get(spotType) || []; for (const spot of spots) { if (spot.isAvailable()) { return spot; } } } return null; }} // Ticket for multi-spot reservationsclass MultiSpotTicket extends Ticket { readonly spotIds: string[]; // All reserved spots constructor( ticketId: string, vehicle: IVehicle, spots: IParkingSpot[], entryTime: Date ) { super(ticketId, vehicle, spots[0], entryTime); this.spotIds = spots.map(s => s.id); }}What happens when payment fails at the exit gate? The vehicle can't leave, but the spot is still occupied. This creates a state inconsistency that needs careful handling.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
// ============================================// PAYMENT STATE MACHINE// ============================================ enum ExitState { TICKET_VALIDATED = 'TICKET_VALIDATED', FEE_CALCULATED = 'FEE_CALCULATED', PAYMENT_PENDING = 'PAYMENT_PENDING', PAYMENT_FAILED = 'PAYMENT_FAILED', PAYMENT_SUCCESS = 'PAYMENT_SUCCESS', GATE_OPENED = 'GATE_OPENED', EXITED = 'EXITED'} interface IExitTransaction { readonly id: string; readonly ticketId: string; readonly fee: Money; state: ExitState; paymentAttempts: PaymentAttempt[];} interface PaymentAttempt { timestamp: Date; method: string; success: boolean; errorMessage?: string;} class ExitTransaction implements IExitTransaction { readonly id: string; readonly ticketId: string; readonly fee: Money; state: ExitState; paymentAttempts: PaymentAttempt[] = []; constructor(ticketId: string, fee: Money) { this.id = ExitTransactionIdGenerator.generate(); this.ticketId = ticketId; this.fee = fee; this.state = ExitState.TICKET_VALIDATED; } transitionTo(newState: ExitState): void { // Validate state transitions const allowed = this.getAllowedTransitions(); if (!allowed.includes(newState)) { throw new InvalidStateTransitionError( `Cannot transition from ${this.state} to ${newState}` ); } this.state = newState; } private getAllowedTransitions(): ExitState[] { switch (this.state) { case ExitState.TICKET_VALIDATED: return [ExitState.FEE_CALCULATED]; case ExitState.FEE_CALCULATED: return [ExitState.PAYMENT_PENDING]; case ExitState.PAYMENT_PENDING: return [ExitState.PAYMENT_SUCCESS, ExitState.PAYMENT_FAILED]; case ExitState.PAYMENT_FAILED: return [ExitState.PAYMENT_PENDING, ExitState.PAYMENT_SUCCESS]; case ExitState.PAYMENT_SUCCESS: return [ExitState.GATE_OPENED]; case ExitState.GATE_OPENED: return [ExitState.EXITED]; default: return []; } } recordPaymentAttempt( method: string, success: boolean, errorMessage?: string ): void { this.paymentAttempts.push({ timestamp: new Date(), method, success, errorMessage }); this.transitionTo( success ? ExitState.PAYMENT_SUCCESS : ExitState.PAYMENT_FAILED ); }} // Payment service with retry logicinterface IPaymentService { processPayment(amount: Money, method: PaymentMethod): PaymentResult;} enum PaymentMethod { CASH = 'CASH', CREDIT_CARD = 'CREDIT_CARD', DEBIT_CARD = 'DEBIT_CARD', MOBILE_PAY = 'MOBILE_PAY', VALIDATION = 'VALIDATION' // Pre-paid validation ticket} interface PaymentResult { success: boolean; transactionId?: string; errorCode?: string; errorMessage?: string; canRetry: boolean;} class ExitGateController { private readonly parkingLot: ParkingLot; private readonly paymentService: IPaymentService; private readonly pendingTransactions: Map<string, ExitTransaction> = new Map(); async initiateExit(ticketId: string): Promise<ExitTransaction> { const ticket = this.parkingLot.getTicket(ticketId); if (!ticket) { throw new InvalidTicketError(`Ticket ${ticketId} not found`); } const fee = this.parkingLot.calculateFee(ticket); const transaction = new ExitTransaction(ticketId, fee); transaction.transitionTo(ExitState.FEE_CALCULATED); this.pendingTransactions.set(transaction.id, transaction); return transaction; } async processPayment( transactionId: string, method: PaymentMethod ): Promise<ExitTransaction> { const transaction = this.pendingTransactions.get(transactionId); if (!transaction) { throw new TransactionNotFoundError(transactionId); } transaction.transitionTo(ExitState.PAYMENT_PENDING); // Attempt payment const result = await this.paymentService.processPayment( transaction.fee, method ); transaction.recordPaymentAttempt(method, result.success, result.errorMessage); if (result.success) { await this.completeExit(transaction); } return transaction; } private async completeExit(transaction: ExitTransaction): Promise<void> { // Free the spot this.parkingLot.freeSpotForTicket(transaction.ticketId); // Open gate transaction.transitionTo(ExitState.GATE_OPENED); await this.gateController.openExitGate(); // Wait for vehicle to pass await this.gateController.waitForVehicleToPass(); // Complete transaction transaction.transitionTo(ExitState.EXITED); this.pendingTransactions.delete(transaction.id); } async escalateToAttendant(transactionId: string): Promise<void> { const transaction = this.pendingTransactions.get(transactionId); if (!transaction) return; // Alert attendant, keep transaction pending await this.attendantService.notifyPaymentIssue(transaction); // Attendant can manually override, use alternate payment, etc. }}The explicit state machine for exit transactions provides: 1) Clear visibility into where the process is stuck, 2) Valid transition enforcement (can't open gate before payment), 3) Audit trail of attempts, 4) Recovery paths (retry or escalate). This is how production systems handle complex workflows.
Modern parking systems need to support electric vehicles with charging stations. This extension demonstrates how our design accommodates new vehicle types and spot capabilities.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
// ============================================// ELECTRIC VEHICLE & CHARGING EXTENSION// ============================================ // Extended vehicle typeenum VehicleType { MOTORCYCLE = 'MOTORCYCLE', CAR = 'CAR', BUS = 'BUS', ELECTRIC_CAR = 'ELECTRIC_CAR', // New type} // New spot typeenum SpotType { MOTORCYCLE = 'MOTORCYCLE', COMPACT = 'COMPACT', LARGE = 'LARGE', ELECTRIC = 'ELECTRIC', // Has charging station} // Electric vehicle with charging needsinterface IElectricVehicle extends IVehicle { readonly batteryLevel: number; // 0-100 readonly requiresCharging: boolean; readonly maxChargingRate: number; // kW} class ElectricVehicle implements IElectricVehicle { readonly licensePlate: string; readonly type = VehicleType.ELECTRIC_CAR; readonly batteryLevel: number; readonly maxChargingRate: number; constructor( licensePlate: string, batteryLevel: number, maxChargingRate: number = 50 // kW ) { this.licensePlate = licensePlate; this.batteryLevel = batteryLevel; this.maxChargingRate = maxChargingRate; } get requiresCharging(): boolean { return this.batteryLevel < 20; } getCompatibleSpotTypes(): SpotType[] { // EVs prefer electric spots, can use regular if needed if (this.requiresCharging) { return [SpotType.ELECTRIC, SpotType.COMPACT]; } return [SpotType.COMPACT, SpotType.ELECTRIC, SpotType.LARGE]; } getPreferredSpotType(): SpotType { return this.requiresCharging ? SpotType.ELECTRIC : SpotType.COMPACT; }} // Electric parking spot with charging capabilityinterface IChargingSpot extends IParkingSpot { readonly chargingPower: number; // kW readonly chargingPortType: ChargingPortType; isCharging(): boolean; startCharging(): void; stopCharging(): void; getEnergyDelivered(): number; // kWh} enum ChargingPortType { TYPE_1 = 'TYPE_1', TYPE_2 = 'TYPE_2', CCS = 'CCS', CHADEMO = 'CHADEMO',} class ElectricParkingSpot extends ParkingSpot implements IChargingSpot { readonly chargingPower: number; readonly chargingPortType: ChargingPortType; private charging: boolean = false; private chargingStartTime: Date | null = null; private totalEnergyDelivered: number = 0; constructor( floor: number, row: number, spotNumber: number, chargingPower: number = 50, portType: ChargingPortType = ChargingPortType.CCS ) { super(floor, row, spotNumber, SpotType.ELECTRIC); this.chargingPower = chargingPower; this.chargingPortType = portType; } isCharging(): boolean { return this.charging; } startCharging(): void { if (!this.isAvailable() === false) { throw new Error('Cannot charge: no vehicle parked'); } this.charging = true; this.chargingStartTime = new Date(); } stopCharging(): void { if (this.charging && this.chargingStartTime) { const hoursCharged = (Date.now() - this.chargingStartTime.getTime()) / (1000 * 60 * 60); this.totalEnergyDelivered += hoursCharged * this.chargingPower; } this.charging = false; this.chargingStartTime = null; } getEnergyDelivered(): number { return this.totalEnergyDelivered; } override vacate(): IVehicle | null { if (this.charging) { this.stopCharging(); } return super.vacate(); }} // Pricing that includes charging feesclass EVPricingStrategy implements IPricingStrategy { private readonly basePricing: IPricingStrategy; private readonly chargingRatePerKwh: number; constructor(basePricing: IPricingStrategy, chargingRate: number = 0.35) { this.basePricing = basePricing; this.chargingRatePerKwh = chargingRate; } calculateFee(ticket: ITicket): Money { const baseFee = this.basePricing.calculateFee(ticket); // Add charging fees if applicable if (ticket instanceof EVTicket && ticket.chargingUsed) { const chargingFee = ticket.energyDelivered * this.chargingRatePerKwh; return baseFee.add(new Money(chargingFee)); } return baseFee; } getDisplayName(): string { return 'EV Parking + Charging'; } getDescription(): string { return `Parking fee plus $${this.chargingRatePerKwh}/kWh for charging`; }}Many parking systems allow advance reservations. This extension shows how to add reservation capability while maintaining backward compatibility with walk-in parking.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
// ============================================// RESERVATION SYSTEM EXTENSION// ============================================ enum ReservationStatus { PENDING = 'PENDING', CONFIRMED = 'CONFIRMED', CHECKED_IN = 'CHECKED_IN', COMPLETED = 'COMPLETED', CANCELLED = 'CANCELLED', NO_SHOW = 'NO_SHOW',} interface IReservation { readonly reservationId: string; readonly vehiclePlate: string; readonly vehicleType: VehicleType; readonly startTime: Date; readonly endTime: Date; status: ReservationStatus; assignedSpotId?: string;} class Reservation implements IReservation { readonly reservationId: string; readonly vehiclePlate: string; readonly vehicleType: VehicleType; readonly startTime: Date; readonly endTime: Date; status: ReservationStatus = ReservationStatus.PENDING; assignedSpotId?: string; constructor( vehiclePlate: string, vehicleType: VehicleType, startTime: Date, endTime: Date ) { this.reservationId = ReservationIdGenerator.generate(); this.vehiclePlate = vehiclePlate; this.vehicleType = vehicleType; this.startTime = startTime; this.endTime = endTime; } isActiveAt(time: Date): boolean { return time >= this.startTime && time <= this.endTime && this.status === ReservationStatus.CONFIRMED; } confirm(spotId: string): void { this.assignedSpotId = spotId; this.status = ReservationStatus.CONFIRMED; } checkIn(): void { if (this.status !== ReservationStatus.CONFIRMED) { throw new Error('Cannot check in non-confirmed reservation'); } this.status = ReservationStatus.CHECKED_IN; } cancel(): void { if (this.status === ReservationStatus.CHECKED_IN || this.status === ReservationStatus.COMPLETED) { throw new Error('Cannot cancel reservation in progress'); } this.status = ReservationStatus.CANCELLED; }} interface IReservationService { createReservation( vehiclePlate: string, vehicleType: VehicleType, startTime: Date, endTime: Date ): Reservation; getReservation(reservationId: string): Reservation | null; checkIn(reservationId: string): Ticket; cancelReservation(reservationId: string): void; getReservationsForTimeRange(start: Date, end: Date): Reservation[];} class ReservationService implements IReservationService { private readonly reservations: Map<string, Reservation> = new Map(); private readonly parkingLot: ParkingLot; private readonly gracePeriodMinutes: number = 15; constructor(parkingLot: ParkingLot) { this.parkingLot = parkingLot; } createReservation( vehiclePlate: string, vehicleType: VehicleType, startTime: Date, endTime: Date ): Reservation { // Validate time if (startTime >= endTime) { throw new ValidationError('Start time must be before end time'); } if (startTime < new Date()) { throw new ValidationError('Cannot create reservation in the past'); } // Check availability for that time window const available = this.checkAvailability(vehicleType, startTime, endTime); if (!available.hasSpot) { throw new NoAvailableSpotError( `No spots available for ${vehicleType} during requested time` ); } const reservation = new Reservation( vehiclePlate, vehicleType, startTime, endTime ); // Reserve the spot reservation.confirm(available.spotId!); this.reservations.set(reservation.reservationId, reservation); return reservation; } private checkAvailability( vehicleType: VehicleType, startTime: Date, endTime: Date ): { hasSpot: boolean; spotId?: string } { // Get all reservations that overlap with requested time const overlapping = this.getReservationsForTimeRange(startTime, endTime); const reservedSpotIds = new Set( overlapping.map(r => r.assignedSpotId).filter(Boolean) ); // Find a spot that's not reserved during that time const spots = this.parkingLot.getCompatibleSpots(vehicleType); for (const spot of spots) { if (!reservedSpotIds.has(spot.id) && spot.isAvailable()) { return { hasSpot: true, spotId: spot.id }; } } return { hasSpot: false }; } checkIn(reservationId: string): Ticket { const reservation = this.reservations.get(reservationId); if (!reservation) { throw new ReservationNotFoundError(reservationId); } const now = new Date(); const graceStart = new Date( reservation.startTime.getTime() - this.gracePeriodMinutes * 60 * 1000 ); if (now < graceStart) { throw new TooEarlyError( `Check-in opens ${this.gracePeriodMinutes} minutes before start time` ); } // Park in the reserved spot const vehicle = VehicleFactory.create( reservation.vehiclePlate, reservation.vehicleType ); const spot = this.parkingLot.getSpot(reservation.assignedSpotId!); spot.park(vehicle); reservation.checkIn(); // Create ticket linked to reservation return new ReservationTicket( TicketIdGenerator.generate(), vehicle, spot, now, reservation ); } getReservation(reservationId: string): Reservation | null { return this.reservations.get(reservationId) || null; } cancelReservation(reservationId: string): void { const reservation = this.reservations.get(reservationId); if (reservation) { reservation.cancel(); } } getReservationsForTimeRange(start: Date, end: Date): Reservation[] { const result: Reservation[] = []; for (const reservation of this.reservations.values()) { // Check if reservation overlaps with range if (reservation.startTime < end && reservation.endTime > start && reservation.status === ReservationStatus.CONFIRMED) { result.push(reservation); } } return result; }}When designing any parking lot system (or similar LLD problem), use this checklist to ensure you've considered the important edge cases:
| Category | Edge Case | Consideration |
|---|---|---|
| Concurrency | Same spot allocated twice | Use locking or optimistic concurrency |
| Concurrency | Entry and exit at same moment | Transaction isolation for spot status |
| Tickets | Lost ticket | License plate lookup + surcharge pricing |
| Tickets | Damaged/unreadable ticket | Manual entry + attendant override |
| Tickets | Fraudulent ticket | Validation against active tickets |
| Vehicles | Oversized vehicle | Multi-spot allocation or rejection |
| Vehicles | Mismatch type declared vs actual | Attendant verification, camera systems |
| Vehicles | Vehicle breakdown in lot | Mark spot as unavailable, don't time-bill |
| Payment | Payment failure | State machine, retry, attendant escalation |
| Payment | Insufficient funds | Alternative payment methods |
| Payment | Payment dispute | Audit logs, receipt generation |
| Capacity | Lot full, vehicle inside | Queue management, wait time estimates |
| Capacity | Last spot race condition | Only one should succeed |
| Time | Overnight parking | Day boundary handling in pricing |
| Time | Timezone changes (DST) | UTC storage, local display |
| Time | System clock issues | NTP sync, sanity checks on duration |
| System | Database failure | Graceful degradation, manual mode |
| System | Power outage | Gate default behavior, recovery |
| System | Network partition (distributed) | Eventual consistency, local operation |
What's next:
With edge cases addressed and extensions designed, the final page brings everything together in a complete design walkthrough—the kind you'd present in an interview or design review.
You now understand how to handle edge cases and extend the base parking lot design. This production-minded thinking differentiates senior designers from those who only handle the happy path. Next, we'll consolidate everything into a complete design walkthrough.