Skip to content

Low-Level Design (LLD): Parking Lot System

The classic first LLD problem, built in layers with controllers, services and repositories, and a Java simulation you can run.

Builds on Design Pattern: Adapter (Structural) and Dependency Inversion Principle (DIP)

Stage 4core10 min read

Introduction

Low-Level Design (LLD) interviews evaluate your ability to model real-world systems into scalable, extensible, and maintainable code. One of the most popular examples used in interviews is the Parking Lot System.

This document walks you through a comprehensive 9-step approach to break down the problem just like you would in an actual interview. At every step, practical tips are included to help you convey your thought process confidently.


Architectural Overview (5-Layer Architecture)

The system is organized into a structured 5-layer enterprise architecture, ensuring strict separation of concerns and modularity:

[ Client Layer ] ──> [ Controller Layer ] ──> [ Service Layer ] ──> [ Repository Layer ] ──> [ Domain Layer ]
                             │                      │
                             └──────────────> [ Adapter Layer ] (Payment Gateways)

Layer Responsibilities

  1. Client Layer: Responsible for user interaction and presenting information to the user (e.g. CLI, Mobile App, Kiosk).
  2. Controller Layer: Handles incoming requests from the client and delegates tasks to the appropriate service components (EntryController, ExitController, AdminController).
  3. Service Layer: Contains core business logic such as ticket generation, slot allocation, fee calculation, and payment processing (TicketService, SlotService, PricingService, PaymentService, ReceiptService, AdminService).
  4. Adapter Layer: Decouples external third-party services like payment gateways (PaymentGatewayAdapter, RazorpayAdapter, StripeAdapter).
  5. Repository Layer: Manages in-memory data access, querying, and persistence (SlotRepository, TicketRepository, FloorRepository, PricingRuleRepository, PaymentRepository).
  6. Domain Layer: Defines the core entities and enums (Vehicle, ParkingSlot, Floor, Ticket, Receipt, PricingRule, Payment).

Step 1: Clarify Requirements

Functional Requirements

1. Entry Flow

  • Vehicle Arrival: Vehicle arrives at an entry gate.
  • Slot Allocation: Assign slot based on vehicle type (BIKE, CAR, TRUCK, EV).
  • Ticket Generation: Generate a parking ticket with entry timestamp.
  • Slot Occupancy: Mark allocated slot as occupied.
  • Response: Return entry response (EntryResult) with ticket details or error if full.

2. Exit Flow

  • Present Ticket: Driver presents ticket at exit gate.
  • Calculate Fee: Calculate fee using pricing rules (flat vs hourly vs hybrid rate).
  • Process Payment: Process payment with retry and fallback support.
  • Release Slot: Release slot and mark as available upon successful payment.
  • Receipt Generation: Deactivate ticket and generate receipt.
  • Response: Return exit response (ExitResult) with receipt summary.

3. Admin Flow

  • Infrastructure Management: Add, edit, or delete floors and slots.
  • Pricing Rules: Define and dynamically update pricing rules per vehicle category.
  • Status Monitoring: View real-time parking lot occupancy and status statistics.

Non-Functional Requirements

  • Scalability: Capable of supporting multiple floors and thousands of slots.
  • Availability: Entry and exit operations must remain functional even during temporary payment gateway failures.
  • Consistency: Thread-safe slot state management to ensure accurate slot status at all times and avoid double-booking.
  • Extensibility: Easy addition of new vehicle types, slot types, or payment gateways without modifying existing core logic.
  • Security: Role-based access control for administrative operations.
  • Latency: Under 500ms execution latency for key entry and exit flows.

Edge Cases to Consider

  • Payment failure at exit (network error / card declined)
  • Lost ticket
  • System clock mismatch / time skew
  • Slot marked occupied incorrectly (state discrepancy)
  • Vehicle-slot type mismatch (e.g. truck attempting to park in bike slot)

Step 2: Identify Core Entities & Domain Model

Entity Specification Table

EntityKey Attributes & TypesDescription & ResponsibilitiesCore Methods & Behaviors
Vehicleid: UUID licensePlate: String vehicleType: VehicleTypeCaptures vehicle metadata entering the facility.getId(), getLicensePlate(), getVehicleType()
ParkingSlotid: UUID slotType: VehicleType isOccupied: boolean floorNumber: intRepresents an individual parking spot on a specific floor level.setOccupied(boolean), isOccupied(), getFloorNumber()
Floorid: UUID floorNumber: int slots: List<ParkingSlot>Represents a physical level containing a collection of parking slots.addSlot(ParkingSlot), getAvailableSlots(VehicleType), getTotalSlots()
Ticketid: UUID vehicleId: UUID slotId: UUID entryTime: LocalDateTime isActive: booleanActs as proof of entry, tracks duration, and links vehicle to slot.deactivate(), isActive(), getEntryTime(), getSlotId()
Receiptid: UUID ticketId: UUID exitTime: LocalDateTime totalFee: double paymentStatus: PaymentStatusProof of payment issued upon vehicle checkout.markAsPaid(), getTotalFee(), getExitTime(), getPaymentStatus()
PricingRuleid: UUID vehicleType: VehicleType ratePerHour: double flatRate: doubleConfigures flat base fee and hourly rates for a vehicle classification.updateRates(double, double), updateFlatRate(double), updateHourlyRate(double)
Paymentid: UUID ticketId: UUID amount: double gateway: PaymentGateway status: PaymentStatusRepresents an individual payment transaction attempt.markAsSuccess(), markAsFailed(), getStatus(), getAmount()
EntryResultsuccess: boolean ticketId: UUID slotId: UUID message: StringDTO representing the response for a vehicle entry request.isSuccess(), getTicketId(), getSlotId(), getMessage()
ExitResultsuccess: boolean receiptId: UUID fee: double message: StringDTO representing the response for a vehicle exit request.isSuccess(), getReceiptId(), getFee(), getMessage()

Enums

Enum NameValuesPurpose
VehicleTypeBIKE, CAR, TRUCK, EVDefines vehicle categories and compatible parking slot types.
PaymentGatewayRAZORPAY, STRIPEIdentifies external payment gateways supported by the adapter layer.
PaymentStatusPENDING, SUCCESS, FAILEDTracks the transaction lifecycle state for payments and receipts.

Step 3: Domain Relationships, Multiplicities & OOP Types

Source EntityTarget EntityMultiplicityRelationship TypeUML RepresentationBusiness Rationale
FloorParkingSlot1 to *CompositionFloor *-- ParkingSlotA Floor owns its ParkingSlots. A ParkingSlot cannot exist independently without a Floor. If a Floor is removed, its slots are destroyed.
TicketVehicle1 to 1AssociationTicket --> VehicleA Ticket references a single Vehicle by vehicleId.
TicketParkingSlot1 to 1AssociationTicket --> ParkingSlotA Ticket references the specific ParkingSlot reserved by slotId.
TicketReceipt1 to 0..1AssociationTicket --> ReceiptA Ticket produces exactly one Receipt when the vehicle exits and settles payment.
TicketPayment1 to *AggregationTicket o-- PaymentA Ticket can have multiple payment attempts (e.g. retries after a failure). Payments reference ticketId.
PricingRuleVehicleType1 to 1AssociationPricingRule --> VehicleTypeEach vehicle classification has exactly one applicable PricingRule.
PaymentGatewayAdapterRazorpayAdapter1 to 1RealizationRazorpayAdapter ..|&gt; PaymentGatewayAdapterRazorpayAdapter implements the PaymentGatewayAdapter interface.
PaymentGatewayAdapterStripeAdapter1 to 1RealizationStripeAdapter ..|&gt; PaymentGatewayAdapterStripeAdapter implements the PaymentGatewayAdapter interface.
ControllersServices1 to *Aggregation / DependencyController o-- ServiceControllers delegate business operations to injected Service instances.
ServicesRepositories1 to *Aggregation / DependencyService o-- RepositoryServices access and persist domain data through injected Repositories.

Step 4: Visual Interaction Flows & Sequences

1. Vehicle Entry Flow

Loading diagram…

2. Vehicle Exit Flow

Loading diagram…

Step 5: Class Structures & Architectural Layering

Controllers

  • EntryController.enterVehicle(licensePlate, vehicleType): Orchestrates slot allocation and ticket issuance.
  • ExitController.exitVehicle(ticketId): Orchestrates ticket lookup, fee computation, payment settlement, slot release, and receipt generation.
  • AdminController.initializeParkingLot(), addFloor(), addSlotsToFloor(), updatePricingRule(), getParkingStatus(): Administrative entry points.

Services

  • SlotService: Allocates and releases parking slots atomically.
  • TicketService: Generates, retrieves, and deactivates parking tickets.
  • PricingService: Calculates fees based on duration and hybrid pricing rules.
  • PaymentService: Processes payments with retry logic and fallback adapter switching.
  • ReceiptService: Generates and formats settled receipts.
  • AdminService: Handles floor creation, slot provisioning, and status aggregation.

Repositories

  • SlotRepository: Thread-safe in-memory slot storage, atomic allocation, and category statistics.
  • TicketRepository: Thread-safe active ticket lookups and persistence.
  • FloorRepository: Floor storage with secondary index for O(1) floor-level queries.
  • PricingRuleRepository: Rule persistence with VehicleType -> ruleId indexing.
  • PaymentRepository: Payment transaction history and ticket-to-payment mapping.

Interfaces and Adapters

  • PaymentGatewayAdapter: Common target interface for payment execution (pay(UUID ticketId, double amount)).
  • RazorpayAdapter: Concrete adapter integrating Razorpay.
  • StripeAdapter: Concrete adapter integrating Stripe.

Step 6: UML Class Diagrams

1. Entry Flow Class Diagram

Loading diagram…

2. Exit Flow Class Diagram

Loading diagram…

3. Admin Flow Class Diagram

Loading diagram…

Step 7: OOP Principles & Design Patterns

Design Patterns Used

  1. Adapter Pattern (PaymentGatewayAdapter):
  • Purpose: Abstraction of payment gateways.
  • Implementation: The PaymentGatewayAdapter interface standardizes the contract. RazorpayAdapter and StripeAdapter encapsulate vendor-specific implementation details.
  • Advantage: Enables seamless addition of new payment providers (e.g. PayPal, ApplePay) without changing PaymentService.
  1. Repository Pattern (SlotRepository, TicketRepository, etc.):
  • Purpose: Isolation of database/persistence operations from business logic.
  • Implementation: Data structures (ConcurrentHashMap) are encapsulated within repositories, providing clean CRUD methods.
  • Advantage: Easy to swap in-memory storage for relational (PostgreSQL, MySQL) or NoSQL databases.
  1. Service Layer Pattern (SlotService, PricingService, PaymentService, etc.):
  • Purpose: Centralization of core business use cases.
  • Advantage: Ensures controllers remain thin request orchestrators and repositories remain dedicated data access components.

SOLID Principles Applied

  • Single Responsibility Principle (SRP): Each class has one clear responsibility. EntryController handles entry requests, SlotService allocates slots, TicketRepository persists tickets.
  • Open/Closed Principle (OCP): The system is open for extension but closed for modification. Adding a new vehicle type (e.g. BICYCLE) or payment gateway only requires adding an enum/adapter without modifying core service workflows.
  • Liskov Substitution Principle (LSP): Concrete gateway adapters (RazorpayAdapter, StripeAdapter) can seamlessly substitute PaymentGatewayAdapter without breaking caller expectations.
  • Interface Segregation Principle (ISP): Role-specific, lean interfaces (e.g. PaymentGatewayAdapter contains only pay()).
  • Dependency Inversion Principle (DIP): High-level services depend on abstractions (PaymentGatewayAdapter, Repositories) rather than concrete implementations.
  • Encapsulation: Domain entities protect internal state with private fields and validate modifications (e.g. Floor.getSlots() returns a defensive copy).

Two More Patterns Worth Naming

A simpler version of this problem often comes up first, with one ParkingLot class, floors and spots. Two patterns fit that version well, and both still apply here.

  • Composite: A lot has floors, and a floor has spots. Asking a floor "how many spots are free?" works the same way as asking the whole lot, so you can treat the group like a single thing.
  • Strategy: How you pick a spot (nearest to the entrance, first free, cheapest) can sit behind a small interface. Then you can swap the rule without touching the code that parks the car.


Step 8: Handling Edge Cases

Edge CaseProblemMitigation Strategy
Lost TicketDriver loses the physical entry ticket.Provide admin override functionality via AdminController to manually validate vehicle license plate in TicketRepository and process exit.
Payment FailureNetwork timeout or card declined during exit.Implement retry logic through PaymentService.processPaymentWithRetry(maxRetries=3) with automatic fallback to secondary gateway (StripeAdapter).
Mismatched Vehicle TypeIncompatible vehicle assigned to wrong slot.Ensure validation of vehicle type during entry; SlotService.allocateSlot() strictly matches VehicleType to slotType.
Clock / Time SkewDistributed server clock discrepancies skew billing duration.Use a centralized time service (TimeProvider) or server UTC epoch timestamps across all services.
Slot State MismatchPhysical sensor disagrees with database slot state.Implement a periodic reconciliation job to audit and re-sync slot states with physical sensors.
Race Conditions / ConcurrencyTwo vehicles attempting to reserve the last available slot simultaneously.Synchronized and thread-safe slot allocation in SlotRepository.allocateSlot() to guarantee mutual exclusion.

Step 9: Source Code Structure

src/LLD/ParkingLot/
├── README.md
└── src/
    ├── adapter/
    │   ├── PaymentGatewayAdapter.java
    │   ├── RazorpayAdapter.java
    │   └── StripeAdapter.java
    ├── controller/
    │   ├── AdminController.java
    │   ├── EntryController.java
    │   └── ExitController.java
    ├── domain/
    │   ├── Floor.java
    │   ├── ParkingSlot.java
    │   ├── Payment.java
    │   ├── PricingRule.java
    │   ├── Receipt.java
    │   ├── Ticket.java
    │   └── Vehicle.java
    ├── repository/
    │   ├── FloorRepository.java
    │   ├── PaymentRepository.java
    │   ├── PricingRuleRepository.java
    │   ├── SlotRepository.java
    │   └── TicketRepository.java
    ├── service/
    │   ├── AdminService.java
    │   ├── PaymentService.java
    │   ├── PricingService.java
    │   ├── ReceiptService.java
    │   ├── SlotService.java
    │   └── TicketService.java
    └── main/
        └── ParkingLotSimulation.java

Interview Talking Points

  • A very big lot: If someone asks about a lot with a hundred floors, do not scan every spot. Keep a map from slot type to a list of free slots, so finding one is a quick lookup.
  • Two cars, one spot: At a busy entrance, two cars can be handed the same free spot at the same moment. Lock around the "find and take" step, as SlotRepository.allocateSlot() does here.
  • Adding a new kind of vehicle: Say you want electric cars. Add a new vehicle type and a new slot type, then update the mapping between them. The rest of the flow stays as it is.

In this topic

Done with this topic?

How well do you know it?