Low-Level Design: Parking Lot System — Space Management, Ticketing, and Dynamic Pricing

Core Entities

ParkingLot: lot_id, name, address, total_floors, total_spaces, open_time, close_time, timezone. ParkingFloor: floor_id, lot_id, floor_number, total_spaces, available_spaces. ParkingSpace: space_id, floor_id, space_number, type (COMPACT, STANDARD, LARGE, HANDICAPPED, EV_CHARGING, MOTORCYCLE), status (AVAILABLE, OCCUPIED, RESERVED, MAINTENANCE), is_active. Vehicle: vehicle_id, license_plate, type (MOTORCYCLE, COMPACT, STANDARD, SUV, TRUCK), owner_id. ParkingTicket: ticket_id, space_id, vehicle_id, entry_time, exit_time, duration_minutes, base_fee, dynamic_multiplier, total_fee, payment_status (PENDING, PAID, EXEMPTED), payment_method. Reservation: reservation_id, space_id, vehicle_id, reserved_from, reserved_until, status (ACTIVE, USED, EXPIRED, CANCELLED), fee_paid. PricingRule: rule_id, lot_id, vehicle_type, hour_start, hour_end, day_type (WEEKDAY, WEEKEND, HOLIDAY), rate_per_hour, minimum_fee.

Entry and Exit Flow

class ParkingService:
    def vehicle_entry(self, lot_id: int, license_plate: str,
                      vehicle_type: str) -> ParkingTicket:
        with self.db.transaction():
            # Check for pre-existing reservation
            reservation = self.repo.get_active_reservation(
                lot_id, license_plate, datetime.utcnow()
            )
            if reservation:
                space = self.repo.get_space(reservation.space_id)
                reservation.status = ReservationStatus.USED
            else:
                # Find best available space
                space = self.repo.find_available_space(lot_id, vehicle_type)
                if not space:
                    raise LotFullError(f'No {vehicle_type} spaces available')

            space.status = SpaceStatus.OCCUPIED
            self.db.execute(
                'UPDATE parking_floors SET available_spaces = available_spaces - 1 ' +
                'WHERE floor_id = :f', f=space.floor_id
            )
            ticket = ParkingTicket(
                space_id=space.space_id,
                vehicle_id=self._get_or_create_vehicle(license_plate, vehicle_type).vehicle_id,
                entry_time=datetime.utcnow(),
                payment_status=PaymentStatus.PENDING
            )
            self.repo.save(ticket)
            return ticket

    def vehicle_exit(self, ticket_id: int) -> ParkingTicket:
        with self.db.transaction():
            ticket = self.repo.get_ticket(ticket_id, lock=True)
            if ticket.exit_time:
                raise AlreadyExitedError()
            ticket.exit_time = datetime.utcnow()
            ticket.duration_minutes = int(
                (ticket.exit_time - ticket.entry_time).total_seconds() / 60
            )
            ticket.total_fee = self._calculate_fee(ticket)
            space = self.repo.get_space(ticket.space_id)
            space.status = SpaceStatus.AVAILABLE
            self.db.execute(
                'UPDATE parking_floors SET available_spaces = available_spaces + 1 ' +
                'WHERE floor_id = :f', f=space.floor_id
            )
            return ticket

Space Assignment Strategy

When assigning spaces to vehicles without a reservation, use a strategy that optimizes both user convenience and lot utilization. Assignment rules: vehicle type matching: motorcycles get motorcycle spaces first; if full, assign compact. Compact cars get compact or standard. SUVs and trucks require large spaces. EV vehicles get EV charging spaces if available, otherwise standard. Floor preference: minimize walking distance — assign the lowest floor with available spaces of the required type. Within a floor: assign spaces closest to the exit ramp (smaller space number = closer to ramp, stored as a property of each space). VIP/premium spaces (first 5 spaces on floor 1): reserved for users who have paid a premium or loyalty members. Space index: maintain a Redis sorted set per (lot_id, floor_id, vehicle_type) with available space_ids. ZPOPMIN to get and claim the best space atomically. Update Redis and database on each entry/exit. Handicapped spaces: never auto-assign to regular vehicles. Only assignable with valid handicap registration.

Dynamic Pricing and Fee Calculation

Base rate: fetched from PricingRule based on vehicle type, current time, and day type. The rate is the cost per hour. Dynamic multiplier: adjust the base rate based on current lot occupancy. Occupancy 0-50%: 1.0x (base rate). Occupancy 50-75%: 1.2x. Occupancy 75-90%: 1.5x. Occupancy > 90%: 2.0x. Multiplier is computed at entry time and locked in for the stay. Fee calculation: divide the stay into hourly blocks. For each block, apply the base rate for that hour (rates differ by time of day, e.g., peak hours 8-10am and 5-7pm cost more). Round up partial hours (1h 5m billed as 2 hours). Apply minimum fee (e.g., first 15 minutes free; minimum $2 after). Apply the dynamic multiplier to the total. Monthly passes: flat monthly fee, no per-day charges. Stored as a valid_pass flag on the vehicle. Entry validation checks for valid pass before calculating fees. Validation (grace period): certain businesses validate parking — the merchant’s validation code extends a time window (e.g., 2 hours free). Applied as a discount on exit.

Reservations and Real-Time Availability

Advance reservations: users can reserve a specific space up to 30 days in advance. On reservation creation: decrement available_spaces for that time window. On reservation expiry (vehicle doesn’t arrive within 15 minutes of reserved_from): release the space, refund 50%. Real-time availability API: GET /lots/{id}/availability returns: total spaces, available by type, floors with availability. Response is cached in Redis for 10 seconds (updates frequently). WebSocket: live availability pushed to apps as spaces change. Digital signage: lot entrance boards display available space counts per type. Populated by the same API. Overcapacity prevention: available_spaces on ParkingFloor is updated atomically in the same transaction as space status changes. No separate counter can drift out of sync with actual space statuses. Periodic reconciliation job (hourly): counts actual AVAILABLE spaces per floor, compares to the counter, corrects any discrepancy (from bugs, crashes), logs the delta.


{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you prevent a parking space from being double-assigned under concurrent load?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a Redis atomic operation for space assignment. Maintain a Redis sorted set of available space IDs per (lot, floor, vehicle_type). Use ZPOPMIN to atomically claim the best space in O(log n). Since ZPOPMIN is atomic, two concurrent requests cannot receive the same space. Also update the database within a transaction to keep Redis and the database in sync. Periodic reconciliation corrects any drift.”}},{“@type”:”Question”,”name”:”How is dynamic pricing multiplier determined at entry time?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”At the moment of entry, compute the current occupancy percentage: (total_spaces – available_spaces) / total_spaces. Apply the multiplier tier (e.g., > 90% = 2.0x). This multiplier is stored on the ParkingTicket and used at exit fee calculation. Locking in the multiplier at entry is fairer to the customer than applying current occupancy at exit (when the lot may be at a different level). The multiplier reflects the scarcity at the time of demand.”}},{“@type”:”Question”,”name”:”How do you handle a customer who loses their parking ticket?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The operator can look up the vehicle by license plate (captured on entry from ANPR cameras or manual input). The system finds the active ParkingTicket associated with that license plate and the current occupied space. The operator charges a standard fee or the computed fee. For automated exits, use ANPR (Automatic Number Plate Recognition) cameras to allow ticketless exit — the exit camera matches the license plate to the entry record and charges automatically.”}},{“@type”:”Question”,”name”:”How does advance reservation interact with real-time availability?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”When a reservation is created, the space is marked RESERVED and available_spaces is decremented for that time window. The real-time availability query excludes reserved spaces from the available count. On the reserved entry time, the vehicle arrives and the reservation transitions to USED. If the vehicle does not arrive within 15 minutes of the reservation start, a background job releases the space (status back to AVAILABLE, available_spaces incremented) and triggers a partial refund.”}},{“@type”:”Question”,”name”:”Why use a separate available_spaces counter on ParkingFloor instead of counting space statuses?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Counting live from the spaces table (SELECT COUNT(*) WHERE status='AVAILABLE') requires a full table scan on every entry, exit, and availability query. At a 1000-space lot, this is manageable, but at a large airport with 10,000+ spaces and high query rates, it becomes a bottleneck. A counter on ParkingFloor is updated atomically in the same transaction as space status changes, giving O(1) availability reads. A periodic reconciliation job catches any counter drift from bugs or crashes.”}}]}

See also: Shopify Interview Prep

See also: Stripe Interview Prep

See also: Uber Interview Prep

See also: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

Scroll to Top