Loading learning content...
This final page consolidates everything we've learned into a cohesive design that you could present in a 45-minute LLD interview or a technical design review. We'll walk through the complete system from start to finish, emphasizing the narrative flow that makes designs compelling.
A great design presentation isn't just a list of classes—it's a story that takes the audience from problem understanding to elegant solution. Let's tell that story.
By the end of this page, you will be able to present a complete parking lot system design from scratch in an interview setting, articulate design decisions with requirement traceability, draw clear UML diagrams that communicate your architecture, and walk through code implementations that demonstrate your design in action.
Before diving into details, start with a high-level summary. This orients your audience and sets expectations.
System Overview:
We're designing a parking lot management system that handles:
Key Design Decisions:
| Decision | Choice | Rationale |
|---|---|---|
| Vehicle Type Modeling | Composition (enum + class) | Flexibility over rigid inheritance |
| Pricing Logic | Strategy Pattern | Interchangeable algorithms |
| Spot Allocation | Strategy Pattern | Configurable for different optimization goals |
| Object Creation | Factory Pattern | Centralized validation and creation |
| Synchronization | Optimistic locking | Throughput with safety |
| Data Access | Aggregate Root pattern | Invariant protection |
Interviewers appreciate when you start by summarizing what you're going to build. It shows organized thinking and helps them follow along. Spend 30 seconds on the overview before diving into details.
A quick recap of requirements anchors the design in real needs.
Functional Requirements (Scope for This Design):
Non-Functional Requirements:
Out of Scope (Explicitly Deferred):
The class diagram is the heart of any LLD presentation. Build it up gradually, explaining each component as you add it.
Diagram Walkthrough Script:
*"Let me walk you through the core design. At the center is ParkingLot—our aggregate root that orchestrates everything. It owns ParkingSpot instances through composition, meaning spots don't exist outside the lot.
Vehicles are modeled with an IVehicle interface and a Vehicle class—we use composition with a type enum rather than inheritance because vehicle behaviors are identical, just properties differ.
The Ticket captures a parking session. It copies relevant vehicle data rather than holding a reference, making it a self-contained historical record.
Critically, ParkingLot doesn't implement pricing or allocation itself. It delegates to IPricingStrategy and IAllocationStrategy interfaces, allowing us to swap algorithms without touching the lot code. This is the Strategy pattern in action.
VehicleFactory centralizes vehicle creation with validation—that's our Factory pattern."*
Show how objects collaborate through sequence diagrams for the main flows.
Vehicle Entry Flow================== EntryGate ParkingLot AllocationStrategy Spot TicketFactory | | | | | |--parkVehicle(v)->| | | | | | | | | | |--findSpot(v, spots)->| | | | | | | | | | loop: Check compatible spot types | | | | |--isAvailable()->| | | | |<---true---------| | | | |--canFit(v)----->| | | | |<---true---------| | | |<--spot---------------| | | | | | | | | |------park(v)--------------------->| | | | | | | |<--occupied--| | | | | |--createTicket(v, spot, time)------------------------->| | |<--ticket-------------------------------------------------| | | |<--ticket---------| | | [Open Gate, Display Spot Location]Sequence diagrams show object collaboration over time—something class diagrams can't capture. They're excellent for verifying your design handles complete flows before you write any code. In interviews, sketching a quick sequence shows systematic thinking.
Let's examine the Strategy implementations that give our system flexibility.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
// ============================================// CONFIGURING THE PARKING LOT WITH STRATEGIES// ============================================ // Define configurationconst lotConfig: ParkingLotConfig = { name: 'Downtown Parking Garage', address: '123 Main Street', floors: 3, spotsPerFloorByType: new Map([ [SpotType.MOTORCYCLE, 20], [SpotType.COMPACT, 50], [SpotType.LARGE, 10], ]),}; // Configure pricing strategyconst weekdayPricing = new DailyMaxPricingStrategy( { [VehicleType.MOTORCYCLE]: 2, // $2/hour [VehicleType.CAR]: 4, // $4/hour [VehicleType.BUS]: 8, // $8/hour }, { [VehicleType.MOTORCYCLE]: 15, // $15/day max [VehicleType.CAR]: 30, // $30/day max [VehicleType.BUS]: 60, // $60/day max }); const weekendPricing = new HourlyPricingStrategy({ [VehicleType.MOTORCYCLE]: 1.5, // Weekend discount [VehicleType.CAR]: 3, [VehicleType.BUS]: 6,}); // Use contextual strategy that automatically switchesconst pricingStrategy = new ContextualPricingStrategy({ weekday: weekdayPricing, weekend: weekendPricing, holiday: weekdayPricing, // Could be different highDemand: new HourlyPricingStrategy({ [VehicleType.MOTORCYCLE]: 3, [VehicleType.CAR]: 6, [VehicleType.BUS]: 12, }), demandThreshold: 0.85,}); // Configure allocation strategyconst allocationStrategy = new NearestExitStrategy(1, 1, 1); // Create the parking lot with injected strategiesconst parkingLot = new ParkingLot( lotConfig, pricingStrategy, allocationStrategy); // The lot is now ready to use// Strategies can be swapped at runtime if needed // Example: Switch to CompactFirst during peak hoursfunction configurePeakMode(lot: ParkingLot): void { // If lot supports strategy swapping: (lot as any).allocationStrategy = new CompactFirstStrategy(); console.log('Switched to CompactFirst allocation for peak hours');}Here's the complete, integrated implementation you could submit in an interview or use as a reference for production development.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
// ============================================// PARKING LOT SYSTEM - COMPLETE IMPLEMENTATION// ============================================ // ----- ENUMERATIONS ----- enum VehicleType { MOTORCYCLE = 'MOTORCYCLE', CAR = 'CAR', BUS = 'BUS',} enum SpotType { MOTORCYCLE = 'MOTORCYCLE', COMPACT = 'COMPACT', LARGE = 'LARGE',} enum SpotStatus { AVAILABLE = 'AVAILABLE', OCCUPIED = 'OCCUPIED',} enum TicketStatus { ACTIVE = 'ACTIVE', PAID = 'PAID', LOST = 'LOST',} // ----- VALUE OBJECTS ----- class Money { readonly amount: number; readonly currency: string; constructor(amount: number, currency = 'USD') { this.amount = Math.round(amount * 100) / 100; this.currency = currency; } add(other: Money): Money { return new Money(this.amount + other.amount, this.currency); } multiply(factor: number): Money { return new Money(this.amount * factor, this.currency); } toString(): string { return `${this.currency} ${this.amount.toFixed(2)}`; }} // ----- INTERFACES ----- interface IVehicle { readonly licensePlate: string; readonly type: VehicleType; getCompatibleSpotTypes(): SpotType[]; getPreferredSpotType(): SpotType;} interface IParkingSpot { readonly id: string; readonly floor: number; readonly row: number; readonly spotNumber: number; readonly type: SpotType; isAvailable(): boolean; canFit(vehicle: IVehicle): boolean; park(vehicle: IVehicle): void; vacate(): IVehicle | null;} interface ITicket { readonly ticketId: string; readonly vehicleLicensePlate: string; readonly vehicleType: VehicleType; readonly spotId: string; readonly entryTime: Date; getDurationHours(): number; getStatus(): TicketStatus;} interface IPricingStrategy { calculateFee(ticket: ITicket): Money;} interface IAllocationStrategy { findSpot( vehicle: IVehicle, spotsByType: Map<SpotType, IParkingSpot[]> ): IParkingSpot | null;} // ----- ENTITY IMPLEMENTATIONS ----- class Vehicle implements IVehicle { readonly licensePlate: string; readonly type: VehicleType; private static readonly COMPATIBILITY: Map<VehicleType, SpotType[]> = new Map([ [VehicleType.MOTORCYCLE, [SpotType.MOTORCYCLE, SpotType.COMPACT, SpotType.LARGE]], [VehicleType.CAR, [SpotType.COMPACT, SpotType.LARGE]], [VehicleType.BUS, [SpotType.LARGE]], ]); constructor(licensePlate: string, type: VehicleType) { this.licensePlate = licensePlate; this.type = type; } getCompatibleSpotTypes(): SpotType[] { return Vehicle.COMPATIBILITY.get(this.type) || []; } getPreferredSpotType(): SpotType { return this.getCompatibleSpotTypes()[0]; }} class VehicleFactory { static create(plate: string, type: VehicleType): IVehicle { return new Vehicle(plate.toUpperCase().trim(), type); } static createCar(plate: string): IVehicle { return this.create(plate, VehicleType.CAR); } static createMotorcycle(plate: string): IVehicle { return this.create(plate, VehicleType.MOTORCYCLE); } static createBus(plate: string): IVehicle { return this.create(plate, VehicleType.BUS); }} class ParkingSpot implements IParkingSpot { readonly id: string; readonly floor: number; readonly row: number; readonly spotNumber: number; readonly type: SpotType; private status: SpotStatus = SpotStatus.AVAILABLE; private parkedVehicle: IVehicle | null = null; constructor(floor: number, row: number, spotNumber: number, type: SpotType) { this.floor = floor; this.row = row; this.spotNumber = spotNumber; this.type = type; this.id = `F${floor}-R${row}-S${spotNumber}`; } isAvailable(): boolean { return this.status === SpotStatus.AVAILABLE; } canFit(vehicle: IVehicle): boolean { return vehicle.getCompatibleSpotTypes().includes(this.type); } park(vehicle: IVehicle): void { if (!this.isAvailable()) { throw new Error(`Spot ${this.id} is not available`); } if (!this.canFit(vehicle)) { throw new Error(`Vehicle cannot fit in spot ${this.id}`); } this.parkedVehicle = vehicle; this.status = SpotStatus.OCCUPIED; } vacate(): IVehicle | null { const vehicle = this.parkedVehicle; this.parkedVehicle = null; this.status = SpotStatus.AVAILABLE; return vehicle; }} class Ticket implements ITicket { readonly ticketId: string; readonly vehicleLicensePlate: string; readonly vehicleType: VehicleType; readonly spotId: string; readonly entryTime: Date; private status: TicketStatus = TicketStatus.ACTIVE; private exitTime: Date | null = null; private paidAmount: number | null = null; constructor(ticketId: string, vehicle: IVehicle, spot: IParkingSpot, entryTime: Date) { this.ticketId = ticketId; this.vehicleLicensePlate = vehicle.licensePlate; this.vehicleType = vehicle.type; this.spotId = spot.id; this.entryTime = entryTime; } getStatus(): TicketStatus { return this.status; } getDurationHours(): number { const endTime = this.exitTime || new Date(); const diffMs = endTime.getTime() - this.entryTime.getTime(); return Math.ceil(diffMs / (1000 * 60 * 60)); } markExited(exitTime: Date, amount: number): void { this.exitTime = exitTime; this.paidAmount = amount; this.status = TicketStatus.PAID; }} // ----- STRATEGIES ----- type VehicleRates = { [key in VehicleType]: number }; class HourlyPricingStrategy implements IPricingStrategy { private readonly rates: VehicleRates; constructor(rates: VehicleRates) { this.rates = rates; } calculateFee(ticket: ITicket): Money { const hours = ticket.getDurationHours(); const rate = this.rates[ticket.vehicleType]; return new Money(hours * rate); }} class DailyMaxPricingStrategy implements IPricingStrategy { private readonly hourlyRates: VehicleRates; private readonly dailyMaxRates: VehicleRates; constructor(hourlyRates: VehicleRates, dailyMaxRates: VehicleRates) { this.hourlyRates = hourlyRates; this.dailyMaxRates = dailyMaxRates; } calculateFee(ticket: ITicket): Money { const hours = ticket.getDurationHours(); const hourlyRate = this.hourlyRates[ticket.vehicleType]; const dailyMax = this.dailyMaxRates[ticket.vehicleType]; const days = Math.floor(hours / 24); const remainingHours = hours % 24; const dayCharge = days * dailyMax; const hourCharge = Math.min(remainingHours * hourlyRate, dailyMax); return new Money(dayCharge + hourCharge); }} class FirstAvailableStrategy implements IAllocationStrategy { findSpot(vehicle: IVehicle, spotsByType: Map<SpotType, IParkingSpot[]>): IParkingSpot | null { for (const spotType of vehicle.getCompatibleSpotTypes()) { const spots = spotsByType.get(spotType) || []; for (const spot of spots) { if (spot.isAvailable()) { return spot; } } } return null; }} // ----- PARKING LOT (AGGREGATE ROOT) ----- interface ParkingLotConfig { name: string; address: string; floors: number; spotsPerFloorByType: Map<SpotType, number>;} class ParkingLot { readonly name: string; readonly address: string; private readonly spotsByType: Map<SpotType, IParkingSpot[]> = new Map(); private readonly allSpots: Map<string, IParkingSpot> = new Map(); private readonly activeTickets: Map<string, Ticket> = new Map(); private ticketCounter: number = 0; private readonly pricingStrategy: IPricingStrategy; private readonly allocationStrategy: IAllocationStrategy; constructor( config: ParkingLotConfig, pricingStrategy: IPricingStrategy, allocationStrategy: IAllocationStrategy ) { this.name = config.name; this.address = config.address; this.pricingStrategy = pricingStrategy; this.allocationStrategy = allocationStrategy; this.initializeSpots(config); } private initializeSpots(config: ParkingLotConfig): void { for (const type of Object.values(SpotType)) { this.spotsByType.set(type as SpotType, []); } for (let floor = 1; floor <= config.floors; floor++) { let spotNumber = 1; for (const [type, count] of config.spotsPerFloorByType) { for (let i = 0; i < count; i++) { const spot = new ParkingSpot(floor, 1, spotNumber++, type); this.spotsByType.get(type)!.push(spot); this.allSpots.set(spot.id, spot); } } } } parkVehicle(vehicle: IVehicle): Ticket { const spot = this.allocationStrategy.findSpot(vehicle, this.spotsByType); if (!spot) { throw new Error(`No available spot for ${vehicle.type}`); } spot.park(vehicle); const ticketId = `TKT-${++this.ticketCounter}`; const ticket = new Ticket(ticketId, vehicle, spot, new Date()); this.activeTickets.set(ticketId, ticket); return ticket; } exitVehicle(ticket: Ticket): Money { if (!this.activeTickets.has(ticket.ticketId)) { throw new Error(`Invalid ticket: ${ticket.ticketId}`); } const spot = this.allSpots.get(ticket.spotId); spot?.vacate(); const fee = this.pricingStrategy.calculateFee(ticket); ticket.markExited(new Date(), fee.amount); this.activeTickets.delete(ticket.ticketId); return fee; } getAvailableSpots(): Map<SpotType, number> { const result = new Map<SpotType, number>(); for (const [type, spots] of this.spotsByType) { result.set(type, spots.filter(s => s.isAvailable()).length); } return result; } getTotalCapacity(): number { return this.allSpots.size; }} // ----- USAGE EXAMPLE ----- const config: ParkingLotConfig = { name: 'City Center Parking', address: '100 Main St', floors: 2, spotsPerFloorByType: new Map([ [SpotType.MOTORCYCLE, 10], [SpotType.COMPACT, 30], [SpotType.LARGE, 5], ]),}; const pricing = new DailyMaxPricingStrategy( { [VehicleType.MOTORCYCLE]: 2, [VehicleType.CAR]: 4, [VehicleType.BUS]: 8 }, { [VehicleType.MOTORCYCLE]: 15, [VehicleType.CAR]: 30, [VehicleType.BUS]: 60 }); const allocation = new FirstAvailableStrategy();const lot = new ParkingLot(config, pricing, allocation); // Park a carconst myCar = VehicleFactory.createCar('ABC123');const ticket = lot.parkVehicle(myCar);console.log(`Parked at: ${ticket.spotId}`); // Check availabilityconsole.log('Available:', Object.fromEntries(lot.getAvailableSpots())); // Exit (after some time)const fee = lot.exitVehicle(ticket);console.log(`Fee: ${fee.toString()}`);How you present matters as much as what you present. Here's how to structure a 45-minute LLD interview:
| Phase | Time | Focus | Tips |
|---|---|---|---|
| Requirements | 5-7 min | Clarify scope, document constraints | Ask about scale, types, edge cases |
| Entity Identification | 5-7 min | Core entities, attributes | Start with nouns from requirements |
| Class Diagram | 10-15 min | Full design, relationships | Build incrementally; explain as you draw |
| Key Flows | 5-10 min | Sequence for entry/exit | Show collaboration between objects |
| Pattern Application | 5 min | Explain why patterns used | Link patterns to requirements |
| Code (if time) | 5-10 min | Core classes implementation | Focus on critical methods |
| Edge Cases | 5 min | Discuss what could go wrong | Proactively mention concurrency, errors |
Interviewers aren't just evaluating your final design—they're evaluating how you think to observe how you'd work as a colleague. Clear communication, systematic approach, and gracefux adaptation to feedback matter as much as pattern knowledge.
Congratulations! You've completed a comprehensive deep-dive into the Parking Lot System—the quintessential LLD case study. Let's recap what you've mastered:
The Transferable Skills:
Everything you've learned here applies to any LLD problem:
Practice Recommendations:
You now have complete mastery of the Parking Lot System design—from requirements through implementation. This case study will serve as your reference template for approaching any LLD problem with confidence and systematic rigor. Proceed to the Library Management System to practice these skills on a different domain.