system-design46 min read

Design a Parking Lot System: The Complete Guide — A Senior+ Guide | Ayodhyya

Design a Parking Lot System: The Complete Guide

Building an automated, scalable parking management platform — gate control, spot assignment, payment processing, IoT sensors, and real-time availability tracking

Senior+ Guide 50+ min read 10,000+ words Ayodhyya

Table of Contents

  1. Introduction — The Parking Lot Landscape
  2. Functional & Non-Functional Requirements
  3. Capacity Estimation & Back-of-Envelope
  4. Data Model & Storage Schema
  5. High-Level Architecture
  6. API Design
  7. Spot Assignment Algorithm
  8. Entry/Exit Gate Control
  9. Payment Processing
  10. Multi-Floor Navigation
  11. Reservation System
  12. Sensor Integration & IoT
  13. Real-Time Availability Tracking
  14. Monitoring, Logging & Alerting
  15. Security, Auth & Fraud Prevention
  16. Cost Estimation
  17. Testing Strategy
  18. Interview Q&A

1. Introduction — The Parking Lot Landscape

Parking is one of the most overlooked yet critical infrastructure challenges in every major city worldwide. Studies from the International Parking & Mobility Institute estimate that urban drivers spend an average of 17 minutes per trip searching for a parking spot, contributing to roughly 30% of urban traffic congestion. In a world that is rapidly moving toward smart cities, the humble parking lot is undergoing a dramatic transformation — from a static concrete structure into a fully connected, software-driven platform capable of real-time optimization, contactless payment, and autonomous vehicle coordination.

Designing a Parking Lot Management System from the perspective of a senior software engineer is not merely about tracking which spots are occupied. It is about architecting a distributed, event-driven platform that must coordinate hardware (gates, sensors, cameras, LED indicators), handle high-throughput concurrent transactions (thousands of entries and exits per hour), maintain strong consistency for billing and reservations, and deliver sub-second latency for real-time availability dashboards displayed on mobile apps and digital signage throughout the facility.

In this comprehensive guide, we will walk through the entire system design from the ground up. We start with requirements gathering and capacity estimation, model the data, design the APIs, and then dive deep into the algorithms that power spot assignment, gate control sequences, payment processing pipelines, multi-floor vehicle navigation, reservation management, and IoT sensor integration. We will also cover real-time availability tracking using publish-subscribe patterns, monitoring and alerting infrastructure, security considerations, cost estimation for cloud resources, and finally, a rigorous testing strategy. Every section includes C# code examples, data model tables, and Mermaid architecture diagrams so you can translate these designs directly into production code.

The modern parking lot system is essentially a distributed real-time operating system for a physical space. It must coordinate the actions of hundreds of actors — vehicles, drivers, sensors, gates, cameras, displays, and mobile applications — all operating concurrently and asynchronously. The system must make decisions in milliseconds that have physical consequences: raising a steel barrier, illuminating an LED indicator, processing a financial transaction, or updating a digital display that hundreds of drivers are relying on for navigation. The margin for error is razor-thin, and the cost of failure is measured not just in angry customers but in traffic congestion, safety incidents, and revenue loss.

Whether you are preparing for a system design interview at a top-tier technology company, building a parking management solution for a commercial real estate client, or simply looking to deepen your understanding of distributed systems that interact with the physical world, this guide will give you the complete blueprint. The design principles we explore here — event sourcing, CQRS, idempotent APIs, circuit breakers, and IoT edge computing — are transferable to a wide range of similar cyber-physical systems including warehouse management, logistics hubs, and autonomous fleet operations.

The complexity of a parking lot system extends far beyond what most engineers initially realize. Consider the sheer number of concurrent operations happening at any given moment: hundreds of vehicles entering and exiting through multiple gates, thousands of sensors reporting status changes every few seconds, digital signage updating in real-time, mobile users querying availability, and payment processors handling financial transactions with strict consistency requirements. All of this must work together seamlessly, even when individual components fail — a sensor goes offline, a network partition occurs, a payment processor times out, or a gate controller loses power. The system must degrade gracefully, never trapping a vehicle inside or overcharging a customer.

From a business perspective, parking management is a multi-billion dollar industry that is being transformed by technology. Companies like ParkMobile, SpotHero, and Flowbird are building smart parking platforms that integrate with municipal infrastructure, ride-sharing services, and autonomous vehicle fleets. Understanding how to design these systems positions you at the intersection of embedded systems, distributed computing, financial technology, and user experience design — a rare and valuable combination in the software engineering profession.

Why parking lot design is a favorite interview topic: It tests your ability to handle real-time state management, hardware integration, financial transactions, and user experience simultaneously — all in a single, coherent system design.

2. Functional & Non-Functional Requirements

Functional Requirements

  1. Vehicle Entry Flow: A vehicle arrives at the entry gate. The system must identify the vehicle (via license plate recognition, RFID tag, QR code, or manual ticket), assign an available parking spot, record the entry timestamp, and open the gate within 2 seconds.
  2. Vehicle Exit Flow: A vehicle approaches the exit gate. The system validates the parking session, computes the fee based on duration and parking type, processes payment, and opens the exit gate within 3 seconds.
  3. Spot Assignment: The system assigns the nearest available spot of the correct type (compact, regular, handicapped, EV charging, oversized) to the incoming vehicle.
  4. Reservation System: Users can reserve a parking spot in advance via a mobile app or web interface. The reservation guarantees a spot of the selected type for a specified time window.
  5. Real-Time Availability: Display real-time available spot counts per floor, zone, and spot type on mobile apps, digital signage, and third-party integrations.
  6. Payment Processing: Support multiple payment methods including credit/debit cards, mobile wallets (Apple Pay, Google Pay), prepaid credits, and monthly subscription passes.
  7. Dynamic Pricing: Adjust parking rates based on demand, time of day, special events, and occupancy levels.
  8. Multi-Floor Navigation: Provide turn-by-turn guidance to the assigned parking spot using LED indicators, digital displays, and mobile app navigation.
  9. EV Charging Management: Manage EV charging stations, track energy consumption, and integrate with charging billing.
  10. Reporting & Analytics: Generate occupancy reports, revenue analytics, peak hour analysis, and utilization heatmaps for facility operators.

Non-Functional Requirements

RequirementTargetRationale
Availability99.99% (52 min downtime/year)Parking lots operate 24/7; downtime means vehicles cannot enter or exit
Latency (Entry)< 2 seconds gate openDrivers expect immediate access; long waits cause queue buildup
Latency (Exit)< 3 seconds gate openSlightly higher tolerance but must still be fast
Throughput500 entries/exits per hour per gate clusterPeak hour at a large facility with multiple lanes
Data ConsistencyStrong consistency for billing; eventual for availability displayOvercharging or undercharging is unacceptable
Fault ToleranceGraceful degradation; gate can operate in offline modeNetwork failures must not trap vehicles inside the facility
ScalabilitySupport 10,000+ spots across multiple facilitiesEnterprise customers operate large portfolios of parking garages
SecurityPCI DSS compliance for payments; encrypted PIIFinancial data and vehicle identity information must be protected

3. Capacity Estimation & Back-of-Envelope

Assumptions

We design for a large multi-story parking facility in a metropolitan area with the following characteristics:

  • Total capacity: 5,000 parking spots across 5 floors
  • Average daily transactions: 8,000 entry/exit pairs
  • Peak hour throughput: 1,200 entries + 1,200 exits
  • Average parking duration: 3 hours
  • Each parking event generates approximately 1 KB of data (metadata, timestamps, billing)
  • Real-time availability updates: every 5 seconds per spot (state change events)

Storage Calculations

Data TypeDaily VolumeSize per RecordDaily StorageMonthly Storage
Parking Session Records8,0001 KB8 MB240 MB
Payment Transactions8,000500 B4 MB120 MB
Sensor Events (raw)86.4M (5s intervals × 5000 spots)100 B8.64 GB259 GB
License Plate Images16,000 (entry + exit)200 KB3.2 GB96 GB
Audit Logs50,000300 B15 MB450 MB
Important: Raw sensor event data is extremely high-volume. In production, we apply edge filtering at the IoT gateway level to only transmit state-change events (occupied ↔ free), reducing daily raw data from 8.64 GB to approximately 80 MB of meaningful transitions.

Bandwidth Calculations

Ingress (sensor to cloud): With edge-filtered events, approximately 80 MB/day = ~1 KB/s average, with peak bursts of up to 50 KB/s during rush hour when many vehicles enter simultaneously. This is well within the capacity of standard LTE/5G or wired connections at each floor's IoT gateway.

Egress (cloud to clients): Real-time availability broadcasts to 1,000 concurrent mobile app users at 5-second intervals: 1,000 × 2 KB payload / 5 seconds = 400 KB/s. Digital signage feeds add another 100 KB/s. Total egress is approximately 500 KB/s, manageable through a CDN or WebSocket connections via a message broker.

QPS Estimates

OperationAvg QPSPeak QPS
Spot Assignment Queries~0.1~3
Availability Status Reads~200~2,000
Availability State Updates~0.1~3
Payment Processing~0.1~1
Gate Commands~0.1~3
Reservation API Calls~1~20

4. Data Model & Storage Schema

The data model must capture the physical hierarchy of the parking facility (building → floor → zone → spot), the lifecycle of a parking session, payment and billing records, reservation entries, sensor telemetry, and user accounts. We use a combination of relational databases for transactional data and time-series databases for sensor telemetry. The schema design must balance normalization for data integrity with denormalization for query performance, especially for real-time availability queries that must return results in under 50 milliseconds.

The hierarchical structure of ParkingFacility → Floor → Zone → ParkingSpot is fundamental to the system. Each facility contains multiple floors, each floor is divided into zones (typically identified by letters or colors for easy driver navigation), and each zone contains individual parking spots with specific dimensions and capabilities. This hierarchy allows the assignment algorithm to efficiently narrow down candidate spots and enables the navigation system to provide floor-level and zone-level guidance before directing the driver to the exact spot number. The zone concept also supports dynamic pricing, as premium zones near elevators or exits can command higher rates than zones at the far end of the floor.

Session management is the heart of the billing system. A ParkingSession record transitions through several states during its lifecycle: it begins as Active when the vehicle enters, may transition to PaymentPending when the exit is initiated, then to Completed once payment is confirmed. Edge cases include Overstayed (when a vehicle exceeds its reserved time window), PaymentFailed (when payment processing encounters an error), and Disputed (when a customer contests the charge). Each state transition is immutably logged in an event store, providing a complete audit trail for billing disputes and regulatory compliance.

Core Entities

EntityKey FieldsStorage
ParkingFacilityId, Name, Address, TotalFloors, TotalSpots, GeoLocationPostgreSQL
FloorId, FacilityId, FloorNumber, Level (B3-B1, G, 1-5), TotalSpotsPostgreSQL
ZoneId, FloorId, ZoneCode (A1, B2), SpotType, CapacityPostgreSQL
ParkingSpotId, ZoneId, SpotNumber, SpotType, Status, HasCharger, Width, LengthPostgreSQL + Redis
ParkingSessionId, SpotId, VehicleId, EntryTime, ExitTime, Status, EntryGateId, ExitGateIdPostgreSQL
VehicleId, LicensePlate, OwnerId, VehicleType, Color, Make, Model, RFIDTagIdPostgreSQL
UserId, Name, Email, Phone, PaymentMethods, SubscriptionTierPostgreSQL
PaymentId, SessionId, Amount, Method, Status, TransactionRef, ProcessedAtPostgreSQL
ReservationId, UserId, FacilityId, SpotType, ReservedFrom, ReservedTo, Status, AssignedSpotIdPostgreSQL
SensorReadingId, SpotId, Timestamp, IsOccupied, Confidence, SensorTypeInfluxDB / TimescaleDB
GateId, FacilityId, GateNumber, GateType (Entry/Exit/Both), Status, CurrentStatePostgreSQL + Redis

ParkingSpot Status Enum

C#
public enum SpotStatus
{
    Available = 0,
    Occupied = 1,
    Reserved = 2,
    OutOfService = 3,
    Maintenance = 4,
    ReservedForEV = 5
}

public enum SpotType
{
    Compact = 0,
    Regular = 1,
    Oversized = 2,
    Handicapped = 3,
    EVCharging = 4,
    Motorcycle = 5
}

ParkingSession Entity

C#
public class ParkingSession
{
    public Guid Id { get; set; }
    public Guid SpotId { get; set; }
    public Guid VehicleId { get; set; }
    public Guid EntryGateId { get; set; }
    public Guid? ExitGateId { get; set; }
    public DateTime EntryTime { get; set; }
    public DateTime? ExitTime { get; set; }
    public SessionStatus Status { get; set; }
    public decimal? TotalFee { get; set; }
    public Guid? ReservationId { get; set; }
    public string EntryLicensePlateImage { get; set; }
    public string ExitLicensePlateImage { get; set; }
    public int EntryFloorAssigned { get; set; }
    public string EntryZoneAssigned { get; set; }
}

public enum SessionStatus
{
    Active = 0,
    Completed = 1,
    PaymentPending = 2,
    PaymentFailed = 3,
    Disputed = 4,
    Overstayed = 5
}

Database Indexing Strategy

TableIndexPurpose
ParkingSpot(ZoneId, Status, SpotType)Find available spots in a zone by type
ParkingSession(VehicleId, Status)Lookup active session for a vehicle
ParkingSession(SpotId, EntryTime DESC)Find last session for a spot
Payment(SessionId)Lookup payment for a session
Reservation(FacilityId, ReservedFrom, ReservedTo, Status)Check overlapping reservations
SensorReading(SpotId, Timestamp DESC)Latest sensor reading per spot
Vehicle(LicensePlate) UNIQUEVehicle identification by plate

5. High-Level Architecture

The parking lot system follows a microservices architecture with event-driven communication between services. At the edge, IoT gateways on each floor collect sensor data and relay gate commands. The cloud layer hosts the core business services, data stores, and real-time communication infrastructure.

graph TB subgraph "Edge Layer" S1[Sensors] --> IG1[IoT Gateway Floor 1] S2[Sensors] --> IG2[IoT Gateway Floor 2] G1[Entry Gates] --> GC1[Gate Controller] G2[Exit Gates] --> GC2[Gate Controller] CAM1[LPR Cameras] --> IG1 CAM2[LPR Cameras] --> IG2 LED1[LED Indicators] --> IG1 end subgraph "API Gateway" GW[API Gateway / Load Balancer] end subgraph "Core Services" GW --> VS[Vehicle Service] GW --> AS[Assignment Service] GW --> GS[Gate Service] GW --> PS[Payment Service] GW --> RS[Reservation Service] GW --> AS2[Availability Service] GW --> NS[Notification Service] GW --> US[User Service] end subgraph "Event Bus" AS --> EB[Apache Kafka / Event Bus] GS --> EB PS --> EB RS --> EB IG1 --> EB IG2 --> EB end subgraph "Data Layer" EB --> DB[(PostgreSQL)] EB --> RD[(Redis Cache)] EB --> TS[(InfluxDB Timeseries)] EB --> S3[(S3 Image Storage)] end subgraph "Client Layer" MA[Mobile App] --> GW WEB[Web Dashboard] --> GW DIG[Digital Signage] --> AS2 TP[Third-Party API] --> GW end

Communication between services follows two patterns: synchronous REST/HTTP for request-response interactions where the caller needs an immediate answer (such as the mobile app querying availability or the gate controller requesting a spot assignment), and asynchronous event-driven messaging via Apache Kafka for fire-and-forget operations where the result can be processed later (such as publishing sensor events, recording audit logs, or triggering notifications). This dual communication model ensures that the critical path (entry/exit) remains fast and non-blocking while background processes handle the less time-sensitive work.

The API Gateway serves as the single entry point for all external traffic, handling SSL termination, authentication token validation, rate limiting, request routing, and response caching. It uses a reverse proxy pattern (implemented via NGINX or Envoy) with route-based load balancing that directs traffic to the appropriate microservice based on the URL path prefix. For example, requests to /api/v1/facilities/* route to the Assignment Service, while requests to /api/v1/payments/* route to the Payment Service. The gateway also implements circuit breaker patterns that return graceful error responses when downstream services are unavailable, preventing cascading failures across the system.

The data layer employs a polyglot persistence strategy where each service owns its data and uses the storage technology best suited to its access patterns. PostgreSQL serves as the primary transactional database for session management, billing, and reservations where ACID guarantees are essential. Redis provides low-latency caching for real-time availability counts and distributed locking for concurrent spot assignment. InfluxDB or TimescaleDB stores high-volume time-series sensor data with efficient compression and time-range queries. S3-compatible object storage holds license plate images captured by LPR cameras. This separation ensures that a failure in one data store does not impact other services — for example, if InfluxDB goes down, the core entry/exit flow continues operating normally using the Redis-cached spot statuses.

Service Responsibilities

ServiceResponsibilityKey Technology
Vehicle ServiceVehicle registration, license plate recognition, profile managementPostgreSQL, OCR ML model
Assignment ServiceSpot allocation algorithm, zone routing, navigation instructionsPostgreSQL, Redis, custom algorithm
Gate ServiceGate open/close commands, anti-tailgating logic, barrier status monitoringMQTT, Redis pub/sub
Payment ServiceFee calculation, payment processing, refunds, invoicingStripe/Braintree, PostgreSQL
Reservation ServiceBooking management, conflict resolution, dynamic pricingPostgreSQL, Redis distributed locks
Availability ServiceReal-time occupancy aggregation, WebSocket broadcasts, signage feedsRedis, WebSocket, Kafka
Notification ServiceSMS, push notifications, email alerts for reservations, overstays, paymentsTwilio, Firebase, SendGrid
User ServiceAuthentication, authorization, profile, subscription managementJWT, OAuth2, PostgreSQL

For IoT device management, the system uses MQTT (Message Queuing Telemetry Transport) as the primary protocol for communication between IoT gateways and the cloud MQTT broker. MQTT is ideal for this use case because it is lightweight, supports Quality of Service (QoS) levels for reliable message delivery, handles intermittent connectivity gracefully, and uses minimal bandwidth — critical for battery-powered sensors. The gate controllers and sensor gateways connect to the MQTT broker using TLS-encrypted connections with client certificates for mutual authentication. Messages are organized into a hierarchical topic structure: facility/{facilityId}/floor/{floorId}/sensor/{sensorId}/state for sensor readings and facility/{facilityId}/gate/{gateId}/command for gate control commands. This topic hierarchy allows efficient message routing and enables facility operators to monitor specific floors or gates by subscribing to the appropriate topic prefix.

Key Design Decision: The Assignment Service and Availability Service are separated because assignment requires strong consistency (only one vehicle can be assigned a spot at a time), while availability display can tolerate a few seconds of staleness. This separation allows each service to scale independently and use appropriate consistency models.

6. API Design

All APIs follow RESTful conventions with JSON payloads. Authentication is handled via JWT bearer tokens. Rate limiting is applied per-user and per-facility to prevent abuse. Idempotency keys are required for all mutation operations (entry, exit, payment, reservation) to handle retries safely.

Entry Flow APIs

HTTP
POST /api/v1/facilities/{facilityId}/entry
Content-Type: application/json
Authorization: Bearer {token}
X-Idempotency-Key: {uuid}

{
    "licensePlate": "ABC-1234",
    "vehicleType": "SUV",
    "gateId": "gate-entry-1",
    "entryMethod": "LPR",
    "reservationId": null
}

Response 200:
{
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "assignedSpot": {
        "spotId": "spot-3f-217",
        "floor": 3,
        "zone": "F",
        "number": "217",
        "type": "Regular",
        "latitude": 34.0522,
        "longitude": -118.2437
    },
    "navigationInstructions": [
        "Enter floor 3 via ramp on left",
        "Proceed to Zone F",
        "Spot 217 is on your right"
    ],
    "entryTimestamp": "2026-07-01T10:15:30Z",
    "estimatedRatePerHour": 5.00,
    "gateOpenCommand": "GATE_OPEN_550e8400"
}

Exit Flow APIs

HTTP
POST /api/v1/facilities/{facilityId}/exit
Content-Type: application/json
Authorization: Bearer {token}
X-Idempotency-Key: {uuid}

{
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "gateId": "gate-exit-2",
    "exitMethod": "LPR"
}

Response 200:
{
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "entryTime": "2026-07-01T10:15:30Z",
    "exitTime": "2026-07-01T13:45:00Z",
    "durationMinutes": 209,
    "fee": {
        "baseFee": 15.00,
        "dynamicSurcharge": 2.50,
        "tax": 1.75,
        "totalDue": 19.25,
        "currency": "USD"
    },
    "paymentStatus": "Pending",
    "paymentUrl": "https://pay.ayodhyya.com/session/550e8400",
    "gateOpenCommand": "GATE_OPEN_550e8400"
}

Spot Availability APIs

HTTP
GET /api/v1/facilities/{facilityId}/availability

Response 200:
{
    "facilityId": "fac-001",
    "lastUpdated": "2026-07-01T13:45:05Z",
    "summary": {
        "totalSpots": 5000,
        "availableSpots": 1247,
        "occupiedSpots": 3580,
        "reservedSpots": 173
    },
    "byFloor": [
        {
            "floor": 1,
            "available": 89,
            "occupied": 405,
            "reserved": 6,
            "byType": {
                "Compact": 12,
                "Regular": 55,
                "Oversized": 3,
                "Handicapped": 4,
                "EVCharging": 15
            }
        }
    ]
}

Reservation APIs

HTTP
POST /api/v1/reservations
Content-Type: application/json
Authorization: Bearer {token}
X-Idempotency-Key: {uuid}

{
    "facilityId": "fac-001",
    "spotType": "EVCharging",
    "floorPreference": 2,
    "startTime": "2026-07-02T09:00:00Z",
    "endTime": "2026-07-02T12:00:00Z",
    "vehicleId": "veh-abc123",
    "prepaidAmount": 15.00
}

Response 201:
{
    "reservationId": "res-789xyz",
    "assignedSpot": {
        "spotId": "spot-2e-045",
        "floor": 2,
        "zone": "E",
        "number": "045",
        "type": "EVCharging",
        "hasCharger": true,
        "chargerPowerKW": 22
    },
    "guaranteedUntil": "2026-07-02T09:30:00Z",
    "totalCost": 15.00,
    "status": "Confirmed"
}

Gate Control APIs

HTTP
POST /api/v1/gates/{gateId}/command
Content-Type: application/json
Authorization: Bearer {service-token}
X-Idempotency-Key: {uuid}

{
    "command": "OPEN",
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "duration_seconds": 10,
    "priority": "Normal"
}

Response 200:
{
    "gateId": "gate-entry-1",
    "commandId": "cmd-456",
    "status": "Executed",
    "executedAt": "2026-07-01T10:15:31Z",
    "barrierState": "Open"
}

7. Spot Assignment Algorithm

The spot assignment algorithm is the core intelligence of the parking lot system. It must select the optimal spot for an incoming vehicle considering multiple factors: spot type compatibility, proximity to the entry point, proximity to the driver's destination (if known), floor balancing to distribute load evenly, and reservation honoring. The algorithm runs inside the Assignment Service and is invoked every time a vehicle enters the facility.

Algorithm Design

We use a weighted scoring function that evaluates each candidate spot and selects the one with the highest composite score. The scoring considers:

  • Type Match (weight 0.40): Whether the spot type matches the vehicle type. Exact matches score 1.0, compatible matches (regular spot for compact car) score 0.6, incompatible score 0.0.
  • Proximity to Entry (weight 0.25): Manhattan distance from the entry gate to the spot. Closer spots score higher.
  • Floor Balance (weight 0.20): Spots on floors with higher occupancy rates score higher to distribute vehicles evenly and prevent any single floor from becoming full.
  • Driver Destination (weight 0.15): If the driver specified a destination (e.g., office on floor 3), spots closer to that destination score higher.

C# Implementation

C#
public class SpotAssignmentService
{
    private readonly IParkingSpotRepository _spotRepo;
    private readonly IFacilityConfigRepository _configRepo;
    private readonly IRedisCache _cache;
    private readonly ILogger<SpotAssignmentService> _logger;

    private const double TypeMatchWeight = 0.40;
    private const double ProximityWeight = 0.25;
    private const double FloorBalanceWeight = 0.20;
    private const double DestinationWeight = 0.15;

    public SpotAssignmentService(
        IParkingSpotRepository spotRepo,
        IFacilityConfigRepository configRepo,
        IRedisCache cache,
        ILogger<SpotAssignmentService> logger)
    {
        _spotRepo = spotRepo;
        _configRepo = configRepo;
        _cache = cache;
        _logger = logger;
    }

    public async Task<AssignmentResult> AssignSpotAsync(
        Guid facilityId,
        SpotType requiredType,
        Guid entryGateId,
        Guid? reservationId,
        GeoCoordinate? destination = null)
    {
        var facility = await _configRepo.GetFacilityAsync(facilityId);
        var gate = facility.Gates.First(g => g.Id == entryGateId);

        var candidateSpots = await GetCandidateSpotsAsync(
            facilityId, requiredType, reservationId);

        if (!candidateSpots.Any())
        {
            return AssignmentResult.NoSpotsAvailable(
                facilityId, requiredType);
        }

        var floorOccupancy = await GetFloorOccupancyAsync(facilityId);

        var scoredSpots = candidateSpots.Select(spot =>
        {
            double typeScore = CalculateTypeMatch(
                spot.Type, requiredType);
            double proximityScore = CalculateProximity(
                spot, gate, facility);
            double balanceScore = CalculateFloorBalance(
                spot, floorOccupancy, facility.TotalSpots);
            double destinationScore = CalculateDestination(
                spot, destination);

            double compositeScore =
                (TypeMatchWeight * typeScore) +
                (ProximityWeight * proximityScore) +
                (FloorBalanceWeight * balanceScore) +
                (DestinationWeight * destinationScore);

            return new ScoredSpot
            {
                Spot = spot,
                Score = compositeScore,
                Breakdown = new ScoreBreakdown
                {
                    TypeMatch = typeScore,
                    Proximity = proximityScore,
                    FloorBalance = balanceScore,
                    Destination = destinationScore
                }
            };
        })
        .OrderByDescending(s => s.Score)
        .ToList();

        var selected = scoredSpots.First();

        await ReserveSpotAsync(
            selected.Spot.Id, facilityId);

        _logger.LogInformation(
            "Assigned spot {SpotId} (score: {Score:F3}) " +
            "at floor {Floor} zone {Zone}",
            selected.Spot.Id, selected.Score,
            selected.Spot.FloorNumber, selected.Spot.ZoneCode);

        return AssignmentResult.Success(
            selected.Spot, selected.Score,
            GenerateNavigation(gate, selected.Spot, facility));
    }

    private double CalculateTypeMatch(
        SpotType spotType, SpotType requiredType)
    {
        if (spotType == requiredType) return 1.0;

        var compatibleMap = new Dictionary<SpotType, SpotType[]>
        {
            [SpotType.Regular] = new[] { SpotType.Compact },
            [SpotType.Oversized] = new[] {
                SpotType.Regular, SpotType.Compact },
            [SpotType.Compact] = Array.Empty<SpotType>()
        };

        if (compatibleMap.ContainsKey(spotType) &&
            compatibleMap[spotType].Contains(requiredType))
            return 0.6;

        return 0.0;
    }

    private double CalculateFloorBalance(
        ParkingSpot spot,
        Dictionary<int, double> floorOccupancy,
        int totalSpots)
    {
        if (!floorOccupancy.ContainsKey(spot.FloorNumber))
            return 0.5;

        double occupancyRate =
            floorOccupancy[spot.FloorNumber];
        return 1.0 - occupancyRate;
    }

    private async Task<Dictionary<int, double>>
        GetFloorOccupancyAsync(Guid facilityId)
    {
        var cacheKey =
            $"facility:{facilityId}:floor_occupancy";
        var cached = await _cache
            .GetAsync<Dictionary<int, double>>(cacheKey);
        if (cached != null) return cached;

        var occupancy = await _spotRepo
            .GetFloorOccupancyRatesAsync(facilityId);
        await _cache.SetAsync(cacheKey, occupancy,
            TimeSpan.FromSeconds(30));
        return occupancy;
    }
}

Scoring Visualization

SpotType MatchProximityFloor BalanceDestinationComposite Score
Floor 2, Zone E, Spot 0451.0 × 0.40 = 0.4000.85 × 0.25 = 0.2130.72 × 0.20 = 0.1440.90 × 0.15 = 0.1350.892
Floor 3, Zone F, Spot 2171.0 × 0.40 = 0.4000.70 × 0.25 = 0.1750.65 × 0.20 = 0.1300.95 × 0.15 = 0.1430.848
Floor 1, Zone A, Spot 0120.6 × 0.40 = 0.2400.95 × 0.25 = 0.2380.55 × 0.20 = 0.1100.30 × 0.15 = 0.0450.633
Floor 5, Zone C, Spot 1801.0 × 0.40 = 0.4000.30 × 0.25 = 0.0750.85 × 0.20 = 0.1700.40 × 0.15 = 0.0600.705

8. Entry/Exit Gate Control

Gate control is the most latency-sensitive component of the system. When a vehicle arrives at a gate, the entire flow — license plate recognition, session creation, spot assignment, payment validation (for exit), and gate opening — must complete within 2 to 3 seconds. The gate controller hardware communicates with the cloud via MQTT over a secure TLS connection, with a local fallback controller that can operate independently during network outages.

Entry Gate Sequence

sequenceDiagram participant Vehicle participant Camera as LPR Camera participant GateCtrl as Gate Controller participant Cloud as Cloud Services participant Gate as Barrier Gate Vehicle->>Camera: Arrives at entry lane Camera->>GateCtrl: License plate captured GateCtrl->>Cloud: MQTT: vehicle_arrived event Cloud->>Cloud: Create parking session Cloud->>Cloud: Assign spot (algorithm) Cloud->>GateCtrl: MQTT: gate_open command GateCtrl->>Gate: Open barrier Gate->>Vehicle: Barrier raised Cloud->>Cloud: Publish spot_occupied event

Exit Gate Sequence

sequenceDiagram participant Vehicle participant Camera as LPR Camera participant GateCtrl as Gate Controller participant Cloud as Cloud Services participant Pay as Payment Service participant Gate as Barrier Gate Vehicle->>Camera: Arrives at exit lane Camera->>GateCtrl: License plate captured GateCtrl->>Cloud: MQTT: exit_request event Cloud->>Cloud: Lookup active session Cloud->>Cloud: Calculate fee Cloud->>Pay: Process payment Pay->>Cloud: Payment confirmed Cloud->>GateCtrl: MQTT: gate_open command GateCtrl->>Gate: Open barrier Gate->>Vehicle: Barrier raised Cloud->>Cloud: Release spot, publish event

Gate Controller C# Implementation

C#
public class GateControllerService
{
    private readonly IMqttClient _mqttClient;
    private readonly IParkingSessionService _sessionService;
    private readonly IAssignmentService _assignmentService;
    private readonly IPaymentService _paymentService;
    private readonly IGateRepository _gateRepo;
    private readonly ILocalGateController _localFallback;

    private const int EntryTimeoutMs = 2000;
    private const int ExitTimeoutMs = 3000;

    public async Task HandleVehicleArrivalAsync(
        string licensePlate, Guid gateId)
    {
        var gate = await _gateRepo.GetGateAsync(gateId);

        if (gate.GateType == GateType.Entry)
        {
            await ProcessEntryAsync(
                licensePlate, gate);
        }
        else if (gate.GateType == GateType.Exit)
        {
            await ProcessExitAsync(
                licensePlate, gate);
        }
    }

    private async Task ProcessEntryAsync(
        string licensePlate, Gate gate)
    {
        var session = await _sessionService
            .CreateSessionAsync(new EntryRequest
            {
                LicensePlate = licensePlate,
                GateId = gate.Id,
                FacilityId = gate.FacilityId,
                EntryTime = DateTime.UtcNow
            });

        var assignment = await _assignmentService
            .AssignSpotAsync(
                gate.FacilityId,
                session.RequiredSpotType,
                gate.Id,
                session.ReservationId);

        if (assignment.IsSuccess)
        {
            await OpenGateAsync(gate, session.Id,
                EntryTimeoutMs);
            await PublishEntryEventAsync(
                session, assignment.Spot);
        }
        else
        {
            await RejectEntryAsync(gate,
                "Facility is full");
        }
    }

    private async Task ProcessExitAsync(
        string licensePlate, Gate gate)
    {
        var session = await _sessionService
            .FindActiveSessionAsync(
                gate.FacilityId, licensePlate);

        if (session == null)
        {
            await HandleUnknownVehicleAsync(gate,
                licensePlate);
            return;
        }

        var fee = await _paymentService
            .CalculateFeeAsync(session);

        var paymentResult = await _paymentService
            .ProcessPaymentAsync(session.Id, fee);

        if (paymentResult.IsSuccess)
        {
            await OpenGateAsync(gate, session.Id,
                ExitTimeoutMs);
            await _sessionService
                .CompleteSessionAsync(
                    session.Id, gate.Id);
            await PublishExitEventAsync(session);
        }
        else
        {
            await HandlePaymentFailureAsync(
                gate, session, paymentResult);
        }
    }

    private async Task OpenGateAsync(
        Gate gate, Guid sessionId, int timeoutMs)
    {
        var command = new GateCommand
        {
            GateId = gate.Id,
            Command = GateCommandType.Open,
            SessionId = sessionId,
            DurationSeconds = 10
        };

        var cts = new CancellationTokenSource(
            timeoutMs);

        try
        {
            await _mqttClient.PublishAsync(
                $"gates/{gate.Id}/command",
                JsonSerializer.Serialize(command),
                cts.Token);
        }
        catch (OperationCanceledException)
        {
            _logger.LogWarning(
                "Gate command timed out for {GateId}, " +
                "using local fallback",
                gate.Id);
            await _localFallback
                .OpenGateLocallyAsync(gate.Id);
        }
    }
}
Offline Fallback: Every gate controller runs a local Raspberry Pi or industrial edge device with a cached copy of active sessions (updated every 30 seconds). If the cloud connection is lost, the local controller can validate tickets, process pre-paid sessions, and open gates. Unpaid exits are queued and synced when connectivity is restored.

9. Payment Processing

The Payment Service handles fee calculation, payment authorization, settlement, refunds, and invoicing. It must support multiple payment methods, handle split payments (e.g., employer pays base rate, employee pays surcharge), process monthly subscriptions, and generate receipts. The service follows the Saga pattern for distributed transactions to ensure consistency between session completion, payment processing, and spot release.

Fee Calculation Logic

C#
public class FeeCalculator
{
    private readonly IFacilityConfigRepository _configRepo;
    private readonly IDynamicPricingService _pricingService;

    public async Task<FeeBreakdown> CalculateFeeAsync(
        ParkingSession session,
        DateTime exitTime)
    {
        var facility = await _configRepo
            .GetFacilityAsync(session.FacilityId);
        var config = facility.PricingConfig;

        var duration = exitTime - session.EntryTime;
        var totalHours = Math.Ceiling(
            duration.TotalHours);

        decimal baseFee = 0m;
        var currentRate = config.FirstHourRate;
        var hoursRemaining = (int)totalHours;

        while (hoursRemaining > 0)
        {
            if (hoursRemaining == (int)totalHours)
            {
                baseFee += currentRate;
            }
            else if (hoursRemaining >
                config.FirstHourRate)
            {
                baseFee += config.SubsequentHourRate;
            }
            else
            {
                baseFee += config.MaxDailyRate -
                    baseFee > config.SubsequentHourRate
                    ? config.SubsequentHourRate
                    : Math.Max(0, config.MaxDailyRate - baseFee);
            }
            hoursRemaining--;
        }

        if (totalHours >= config.DailyMaxHours)
        {
            baseFee = Math.Min(
                baseFee, config.DailyMaxRate);
        }

        if (session.EntryDayOfWeek == DayOfWeek.Saturday ||
            session.EntryDayOfWeek == DayOfWeek.Sunday)
        {
            baseFee *= config.WeekendMultiplier;
        }

        var surgeMultiplier = await _pricingService
            .GetSurgeMultiplierAsync(
                session.FacilityId,
                session.EntryTime,
                exitTime);

        decimal surcharge = surgeMultiplier > 1.0m
            ? baseFee * (surgeMultiplier - 1.0m)
            : 0m;

        decimal tax = (baseFee + surcharge) *
            config.TaxRate;

        decimal totalDue = baseFee + surcharge + tax;

        if (session.ReservationId != null)
        {
            totalDue -= config.ReservationDiscount;
        }

        return new FeeBreakdown
        {
            BaseFee = Math.Round(baseFee, 2),
            DynamicSurcharge = Math.Round(surge, 2),
            Tax = Math.Round(tax, 2),
            ReservationDiscount = Math.Round(
                config.ReservationDiscount, 2),
            TotalDue = Math.Round(totalDue, 2),
            Currency = config.Currency,
            RateApplied = currentRate,
            DurationMinutes = (int)duration.TotalMinutes
        };
    }
}

Payment Method Support

MethodProviderSettlementUse Case
Credit/Debit CardStripeT+2 business daysPay-on-exit and pay-online
Apple Pay / Google PayStripe (via Apple/Google)T+2 business daysContactless mobile payment
Prepaid CreditsInternal ledgerInstant deductionCorporate accounts, frequent users
Monthly SubscriptionInternal billingMonthly chargeCommuters, reserved monthly spot
Toll IntegrationE-ZPass / SunPassT+3 business daysHighway parking facilities

Saga Pattern for Payment Flow

sequenceDiagram participant PS as Payment Service participant DB as Database participant Stripe as Stripe API participant NS as Notification Service participant AS as Availability Service PS->>DB: Begin saga: create Payment(Pending) PS->>Stripe: Charge card alt Payment Success Stripe->>PS: Charge confirmed PS->>DB: Update Payment(Success) PS->>DB: Update Session(Completed) PS->>AS: Publish spot_released event PS->>NS: Send receipt email else Payment Failed Stripe->>PS: Charge declined PS->>DB: Update Payment(Failed) PS->>DB: Update Session(PaymentFailed) PS->>NS: Send payment failure alert Note over PS: Retry up to 3 times,
then escalate end

10. Multi-Floor Navigation

Once a spot is assigned, the system must guide the driver from the entry gate to the exact spot. This is accomplished through a combination of floor-level LED indicators, zone-level digital displays, and in-app turn-by-turn navigation. The navigation system must account for one-way ramps, height restrictions, speed bumps, and real-time obstacles.

Navigation Data Model

EntityDescription
NavigationGraphA weighted directed graph where nodes are waypoints (gates, ramps, intersections, spots) and edges are traversable paths with distance and constraint metadata
WaypointA node in the graph with coordinates (x, y, floor), type (gate, ramp, spot, intersection), and accessibility constraints
PathEdgeAn edge connecting two waypoints with distance, direction, allowed vehicle sizes, and real-time obstruction status
LEDDirectionMaps waypoints to LED indicator states (forward, turn-left, turn-right, stop, destination)

Pathfinding Implementation

C#
public class NavigationService
{
    private readonly INavigationGraphRepository _graphRepo;
    private readonly ILEDController _ledController;

    public async Task<NavigationRoute> GenerateRouteAsync(
        Guid facilityId,
        int fromFloor,
        string fromGate,
        int toFloor,
        string toZone,
        string toSpot,
        VehicleDimensions vehicle)
    {
        var graph = await _graphRepo
            .GetNavigationGraphAsync(facilityId);

        var startNode = graph.Waypoints.First(w =>
            w.Floor == fromFloor &&
            w.Label == fromGate);

        var endNode = graph.Waypoints.First(w =>
            w.Floor == toFloor &&
            w.Label == toSpot);

        var validEdges = graph.Edges.Where(e =>
            e.MinVehicleWidth >= vehicle.Width &&
            e.MinVehicleLength >= vehicle.Length &&
            !e.IsObstructed).ToList();

        var path = DijkstraWithConstraints(
            graph.Waypoints, validEdges,
            startNode, endNode);

        var instructions = path
            .SlidingWindow(3)
            .Select(window =>
            {
                var from = window[0];
                var via = window[1];
                var to = window[2];
                var angle = CalculateTurnAngle(
                    from, via, to);

                return new NavigationInstruction
                {
                    AtWaypoint = via.Label,
                    Floor = via.Floor,
                    Action = angle switch
                    {
                        < -30 => TurnAction.TurnLeft,
                        > 30 => TurnAction.TurnRight,
                        _ => TurnAction.ContinueStraight
                    },
                    DistanceMeters = CalculateDistance(
                        via, to),
                    LEDIndicator = via.LEDId,
                    InstructionText = GenerateText(
                        from, via, to)
                };
            }).ToList();

        instructions.Add(new NavigationInstruction
        {
            AtWaypoint = endNode.Label,
            Floor = endNode.Floor,
            Action = TurnAction.Arrived,
            InstructionText =
                $"Spot {toSpot} is on your " +
                $"{"left/right"}"
        });

        await _ledController
            .SetRouteIndicatorsAsync(
                facilityId, instructions);

        return new NavigationRoute
        {
            Waypoints = path.Count,
            TotalDistanceMeters = CalculateTotalDistance(
                path),
            EstimatedTimeSeconds =
                CalculateEstimatedTime(path, vehicle),
            Instructions = instructions
        };
    }

    private List<Waypoint> DijkstraWithConstraints(
        List<Waypoint> waypoints,
        List<PathEdge> edges,
        Waypoint start,
        Waypoint end)
    {
        var distances = waypoints
            .ToDictionary(w => w.Id, _ => double.MaxValue);
        var previous = new Dictionary<Guid, Guid>();
        var visited = new HashSet<Guid>();
        var queue = new SortedSet<(double dist, Guid id)>();

        distances[start.Id] = 0;
        queue.Add((0, start.Id));

        while (queue.Count > 0)
        {
            var current = queue.Min;
            queue.Remove(queue.Min);

            if (visited.Contains(current.id))
                continue;
            visited.Add(current.id);

            if (current.id == end.Id) break;

            var outgoingEdges = edges
                .Where(e => e.FromWaypointId == current.id);

            foreach (var edge in outgoingEdges)
            {
                var newDist = current.dist +
                    edge.Distance;
                if (newDist <
                    distances[edge.ToWaypointId])
                {
                    distances[edge.ToWaypointId] =
                        newDist;
                    previous[edge.ToWaypointId] =
                        current.id;
                    queue.Add((newDist,
                        edge.ToWaypointId));
                }
            }
        }

        return ReconstructPath(
            previous, start.Id, end.Id);
    }
}

11. Reservation System

The reservation system allows users to guarantee a parking spot in advance. It must handle concurrent booking requests without double-booking, support time-window conflicts detection, manage cancellations and refunds, enforce grace periods, and integrate with the spot assignment algorithm to hold spots during the reservation window.

Reservation Lifecycle

stateDiagram-v2 [*] --> Pending : User creates reservation Pending --> Confirmed : Payment successful Pending --> Cancelled : Payment failed Confirmed --> Active : Vehicle arrives (check-in) Confirmed --> Expired : Grace period (30min) exceeded Confirmed --> Cancelled : User cancels Active --> Completed : Vehicle departs Active --> Overstayed : Reserved time exceeded Completed --> [*] Cancelled --> [*] Expired --> [*] Overstayed --> Completed : Excess fee charged

Conflict Detection with Distributed Locking

C#
public class ReservationService
{
    private readonly IReservationRepository _repo;
    private readonly IDistributedLock _lockManager;
    private readonly IPricingService _pricingService;
    private readonly INotificationService _notifications;

    public async Task<ReservationResult> CreateReservationAsync(
        CreateReservationRequest request)
    {
        var lockKey =
            $"facility:{request.FacilityId}:" +
            $"type:{request.SpotType}:" +
            $"date:{request.StartTime:yyyyMMdd}";

        await using var lockHandle = await _lockManager
            .AcquireAsync(lockKey,
                TimeSpan.FromSeconds(10));

        if (lockHandle == null)
        {
            return ReservationResult.Failed(
                "System is busy, please retry");
        }

        var hasConflict = await _repo
            .HasOverlappingReservationAsync(
                request.FacilityId,
                request.SpotType,
                request.FloorPreference,
                request.StartTime,
                request.EndTime);

        if (hasConflict)
        {
            var alternative = await FindAlternativeAsync(
                request);
            return ReservationResult
                .ConflictWithAlternative(alternative);
        }

        var availableSpots = await _repo
            .CountAvailableSpotsAsync(
                request.FacilityId,
                request.SpotType,
                request.StartTime,
                request.EndTime);

        if (availableSpots == 0)
        {
            return ReservationResult.Failed(
                "No spots available for this time window");
        }

        var price = await _pricingService
            .CalculateReservationPriceAsync(
                request.FacilityId,
                request.SpotType,
                request.StartTime,
                request.EndTime);

        var reservation = new Reservation
        {
            Id = Guid.NewGuid(),
            UserId = request.UserId,
            FacilityId = request.FacilityId,
            SpotType = request.SpotType,
            FloorPreference = request.FloorPreference,
            ReservedFrom = request.StartTime,
            ReservedTo = request.EndTime,
            TotalCost = price.TotalCost,
            Status = ReservationStatus.Pending,
            CreatedAt = DateTime.UtcNow,
            GracePeriodMinutes = 30,
            GuaranteedUntil =
                request.StartTime.AddMinutes(30)
        };

        await _repo.CreateAsync(reservation);

        await _notifications.SendAsync(
            request.UserId,
            new ReservationConfirmed
            {
                ReservationId = reservation.Id,
                SpotType = request.SpotType,
                StartTime = request.StartTime,
                TotalCost = price.TotalCost
            });

        return ReservationResult.Success(reservation);
    }

    public async Task CheckAndExpireReservationsAsync()
    {
        var expired = await _repo
            .FindExpiredReservationsAsync(
                DateTime.UtcNow);

        foreach (var reservation in expired)
        {
            reservation.Status =
                ReservationStatus.Expired;
            await _repo.UpdateAsync(reservation);

            if (reservation.TotalCost > 0)
            {
                await ProcessRefundAsync(reservation);
            }

            await _notifications.SendAsync(
                reservation.UserId,
                new ReservationExpired
                {
                    ReservationId = reservation.Id
                });
        }
    }
}

12. Sensor Integration & IoT

IoT sensors are the eyes and ears of the parking lot system. Each parking spot is equipped with an ultrasonic or magnetic sensor that detects vehicle presence. These sensors communicate with floor-level IoT gateways via Bluetooth Low Energy (BLE) or LoRa, which then relay data to the cloud via MQTT over LTE or wired Ethernet. The sensor layer must be robust, energy-efficient, and capable of operating during power outages.

Sensor Types

Sensor TypeDetection MethodAccuracyBattery LifeCost per Unit
UltrasonicSound wave reflection98.5%3-5 years$15-$25
Magnetic (In-ground)Magnetic field disturbance99.2%5-8 years$30-$50
Camera-based (LPR)Computer vision97.0%N/A (wired)$500-$2,000
LiDARLaser distance measurement99.5%N/A (wired)$300-$800
Pressure-sensitive matWeight/pressure detection96.0%2-3 years$40-$60

IoT Gateway Architecture

C#
public class IoTSensorGateway
{
    private readonly IMqttClient _mqttClient;
    private readonly IBleScanner _bleScanner;
    private readonly IStateFilter _stateFilter;
    private readonly ILogger<IoTSensorGateway> _logger;

    private readonly Dictionary<Guid, SpotSensorState>
        _lastKnownState = new();

    private readonly TimeSpan _reportInterval =
        TimeSpan.FromSeconds(5);

    public async Task StartMonitoringAsync(
        int floorNumber,
        CancellationToken ct)
    {
        _logger.LogInformation(
            "Starting IoT gateway for floor {Floor}",
            floorNumber);

        var sensorScan = _bleScanner
            .ScanAsync(floorNumber, ct);

        await foreach (var reading in sensorScan)
        {
            var previousState = _lastKnownState
                .GetValueOrDefault(reading.SensorId);

            if (_stateFilter.HasStateChanged(
                previousState, reading))
            {
                var event = new SensorStateChangedEvent
                {
                    SensorId = reading.SensorId,
                    SpotId = reading.MappedSpotId,
                    FloorNumber = floorNumber,
                    PreviousState =
                        previousState?.IsOccupied,
                    CurrentState = reading.IsOccupied,
                    Confidence = reading.Confidence,
                    Timestamp = DateTime.UtcNow,
                    BatteryLevel = reading.BatteryPercent,
                    SignalStrength = reading.RssiDbm
                };

                await _mqttClient.PublishAsync(
                    $"sensors/floor{floorNumber}/" +
                    $"state_changed",
                    JsonSerializer.Serialize(event));

                _lastKnownState[reading.SensorId] =
                    new SpotSensorState
                    {
                        IsOccupied = reading.IsOccupied,
                        LastUpdate = DateTime.UtcNow,
                        Confidence = reading.Confidence
                    };

                _logger.LogDebug(
                    "Spot {SpotId} changed: " +
                    "{Prev} -> {Curr} " +
                    "(confidence: {Conf:P0})",
                    reading.MappedSpotId,
                    previousState?.IsOccupied,
                    reading.IsOccupied,
                    reading.Confidence);
            }
        }
    }
}

public class StateFilter : IStateFilter
{
    private const double MinConfidence = 0.85;
    private const int DebounceCount = 3;

    private readonly Dictionary<Guid, int>
        _flipCounts = new();

    public bool HasStateChanged(
        SpotSensorState previous,
        SensorReading current)
    {
        if (current.Confidence < MinConfidence)
            return false;

        if (previous == null)
            return true;

        bool stateChanged =
            previous.IsOccupied != current.IsOccupied;

        if (!stateChanged)
        {
            _flipCounts.Remove(current.SensorId);
            return false;
        }

        if (!_flipCounts.ContainsKey(current.SensorId))
            _flipCounts[current.SensorId] = 0;

        _flipCounts[current.SensorId]++;

        if (_flipCounts[current.SensorId] >= DebounceCount)
        {
            _flipCounts.Remove(current.SensorId);
            return true;
        }

        return false;
    }
}
Edge Computing Benefits: By filtering and debouncing sensor readings at the IoT gateway level (edge), we reduce cloud bandwidth by 95% and lower MQTT message volume from approximately 86 million per day to roughly 50,000 meaningful state-change events. This also reduces cloud compute costs significantly.

13. Real-Time Availability Tracking

Real-time availability tracking is the feature that users interact with most frequently. The mobile app, digital signage displays, and third-party integrations all depend on a low-latency stream of occupancy data. We implement this using a Redis-backed aggregation layer that processes sensor events and broadcasts updates via WebSocket connections and Server-Sent Events (SSE).

Architecture for Real-Time Updates

graph LR subgraph "Sensor Events" SE[Kafka Sensor Topic] end subgraph "Aggregation Layer" SE --> AP[Availability Processor] AP --> RC[(Redis Sorted Set
Spot Status)] AP --> FC[(Redis Hash
Floor Counters)] end subgraph "Broadcast Layer" FC --> WP[WebSocket Publisher] FC --> SSE[SSE Broadcaster] FC --> SG[Signage Group Manager] end subgraph "Consumers" WP --> MA[Mobile App 1..N] SSE --> WEB[Web Dashboard] SG --> DS1[Floor 1 Display] SG --> DS2[Floor 2 Display] SG --> DS3[Lobby Display] end

Availability Processor Implementation

C#
public class AvailabilityProcessor : BackgroundService
{
    private readonly IConsumer<string, SensorStateChangedEvent>
        _kafkaConsumer;
    private readonly IConnectionMultiplexer _redis;
    private readonly IWebSocketPublisher _wsPublisher;
    private readonly ILogger<AvailabilityProcessor> _logger;

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var result = await _kafkaConsumer
                .ConsumeAsync(stoppingToken);

            if (result?.Message?.Value == null) continue;

            var evt = result.Message.Value;

            await UpdateSpotStatusAsync(evt);
            await UpdateFloorCountersAsync(evt);
            await BroadcastAvailabilityAsync(
                evt.FacilityId, evt.FloorNumber);
        }
    }

    private async Task UpdateSpotStatusAsync(
        SensorStateChangedEvent evt)
    {
        var db = _redis.GetDatabase();

        var spotKey = $"spot:{evt.SpotId}:status";
        await db.HashSetAsync(spotKey, new HashEntry[]
        {
            new("is_occupied", evt.CurrentState),
            new("confidence", evt.Confidence),
            new("last_update",
                evt.Timestamp.ToUnixTimeMilliseconds()),
            new("sensor_battery", evt.BatteryLevel)
        });

        await db.KeyExpireAsync(spotKey,
            TimeSpan.FromMinutes(30));
    }

    private async Task UpdateFloorCountersAsync(
        SensorStateChangedEvent evt)
    {
        var db = _redis.GetDatabase();
        var counterKey =
            $"facility:{evt.FacilityId}:" +
            $"floor:{evt.FloorNumber}:counters";

        if (evt.CurrentState)
        {
            await db.HashIncrementAsync(
                counterKey, "occupied");
            await db.HashDecrementAsync(
                counterKey, "available");
        }
        else
        {
            await db.HashDecrementAsync(
                counterKey, "occupied");
            await db.HashIncrementAsync(
                counterKey, "available");
        }

        await db.HashSetAsync(counterKey, "last_update",
            DateTimeOffset.UtcNow.ToUnixTimeSeconds());
    }

    private async Task BroadcastAvailabilityAsync(
        Guid facilityId, int floorNumber)
    {
        var db = _redis.GetDatabase();
        var counterKey =
            $"facility:{facilityId}:" +
            $"floor:{floorNumber}:counters";

        var counters = await db
            .HashGetAllAsync(counterKey);

        var update = new AvailabilityUpdate
        {
            FacilityId = facilityId,
            FloorNumber = floorNumber,
            Available = int.Parse(
                counters.First(c =>
                    c.Name == "available").Value),
            Occupied = int.Parse(
                counters.First(c =>
                    c.Name == "occupied").Value),
            Timestamp = DateTime.UtcNow
        };

        await _wsPublisher.PublishAsync(
            $"facility:{facilityId}:availability",
            JsonSerializer.Serialize(update));

        await _wsPublisher.PublishToSignageAsync(
            $"facility:{facilityId}:floor:{floorNumber}",
            update);
    }
}

WebSocket Message Format

JSON
{
    "type": "availability_update",
    "facilityId": "fac-001",
    "floorNumber": 3,
    "available": 127,
    "occupied": 370,
    "reserved": 3,
    "byType": {
        "Compact": 18,
        "Regular": 82,
        "Oversized": 5,
        "Handicapped": 8,
        "EVCharging": 14
    },
    "timestamp": "2026-07-01T13:45:05Z"
}

14. Monitoring, Logging & Alerting

A production parking lot system requires comprehensive observability to detect and respond to issues before they impact users. This includes application metrics, infrastructure health monitoring, hardware sensor status, gate operation success rates, payment processing metrics, and business KPIs like occupancy rates and revenue per hour. We implement the three pillars of observability: metrics, logs, and traces.

Key Metrics Dashboard

MetricAlert ThresholdImpact
Gate Open Latency (p99)> 3 secondsVehicle queue buildup at entrance
Sensor Heartbeat MissingNo data for > 5 minutesIncorrect availability data for that spot
Payment Failure Rate> 2% in 5-minute windowVehicles stuck at exit gate
Kafka Consumer Lag> 1000 messagesStale availability data
Redis Memory Usage> 80%Potential cache eviction and performance degradation
MQTT Connection Drops> 3 reconnects in 1 minuteGate controller losing cloud connectivity
API Error Rate (5xx)> 0.1% in 5 minutesService degradation
Facility Occupancy> 95%Near capacity; trigger overflow handling

C# Telemetry Service

C#
public class ParkingMetricsService
{
    private readonly Meter _meter;
    private readonly Counter<long> _entriesTotal;
    private readonly Counter<long> _exitsTotal;
    private readonly Histogram<double> _gateOpenLatency;
    private readonly Histogram<double> _paymentProcessingTime;
    private readonly UpDownCounter<long> _activeSessions;
    private readonly ObservableGauge<double> _facilityOccupancy;

    public ParkingMetricsService(IMeterFactory meterFactory)
    {
        _meter = meterFactory.Create("ParkingLot.System");

        _entriesTotal = _meter.CreateCounter<long>
            "parking.entries.total",
            "entries", "Total vehicle entries");

        _exitsTotal = _meter.CreateCounter<long>
            "parking.exits.total",
            "exits", "Total vehicle exits");

        _gateOpenLatency = _meter.CreateHistogram<double>
            "parking.gate.open_latency",
            "ms", "Gate open command latency");

        _paymentProcessingTime =
            _meter.CreateHistogram<double>
            "parking.payment.processing_time",
            "ms", "Payment processing time");

        _activeSessions = _meter
            .CreateUpDownCounter<long>
            "parking.sessions.active",
            "sessions", "Currently active sessions");

        _facilityOccupancy = _meter
            .CreateObservableGauge<double>
            "parking.facility.occupancy_pct",
            () => new Measurement<double>(
                CalculateCurrentOccupancy()),
            "%", "Current facility occupancy");
    }

    public void RecordEntry(string facilityId,
        string floor, string spotType)
    {
        _entriesTotal.Add(1, new TagList
        {
            { "facility", facilityId },
            { "floor", floor },
            { "spot_type", spotType }
        });
        _activeSessions.Add(1);
    }

    public void RecordGateOpenLatency(
        double latencyMs, string gateId,
        string gateType)
    {
        _gateOpenLatency.Record(latencyMs,
            new TagList
        {
            { "gate_id", gateId },
            { "gate_type", gateType }
        });
    }
}

Alert Rules (Prometheus-style)

YAML
groups:
  - name: parking_lot_alerts
    rules:
      - alert: GateOpenLatencyHigh
        expr: histogram_quantile(0.99,
          rate(parking_gate_open_latency_seconds_bucket[5m]))
          > 3
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Gate open latency exceeds 3s"
          description: "Gate {{ $labels.gate_id }}
            p99 latency is {{ $value }}s"

      - alert: SensorOffline
        expr: time() - parking_sensor_last_seen_timestamp
          > 300
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Sensor {{ $labels.sensor_id }}
            offline for >5 minutes"

      - alert: PaymentFailureRateHigh
        expr: rate(parking_payment_failures_total[5m])
          / rate(parking_payment_attempts_total[5m]) > 0.02
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "Payment failure rate above 2%"

      - alert: FacilityNearCapacity
        expr: parking_facility_occupancy_pct > 95
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Facility {{ $labels.facility_id }}
            at {{ $value }}% capacity"

Distributed Tracing

Every request that enters the system is assigned a unique trace ID that propagates across all service boundaries, IoT gateway messages, Kafka events, and database queries. This allows operations engineers to reconstruct the complete lifecycle of any parking session — from the moment a vehicle approaches the entry gate, through spot assignment, gate opening, sensor confirmation, and eventual exit with payment. We use OpenTelemetry for instrumentation and Jaeger or Zipkin for trace visualization. Each span in the trace includes metadata such as the gate ID, floor number, assigned spot, payment amount, and processing latency, enabling rapid root cause analysis when issues occur.

Log Aggregation and Structured Logging

All services emit structured JSON logs that are collected by Fluentd or Filebeat agents running on each Kubernetes node and shipped to Elasticsearch or Loki for centralized storage and querying. Every log entry includes the trace ID, span ID, service name, facility ID, and relevant business identifiers (session ID, vehicle ID, gate ID) to enable correlation across services. Critical events such as gate failures, payment errors, sensor malfunctions, and fraud alerts are tagged with severity levels that trigger PagerDuty escalations for on-call engineers. Log retention is configured at 30 days for debug-level logs, 90 days for info-level, and 1 year for error and audit logs to comply with regulatory requirements.

Runbook Automation

When alerts fire, automated runbooks execute predefined remediation steps before escalating to human operators. For example, if a sensor on a particular spot goes offline for more than 5 minutes, the system automatically marks that spot as OutOfService, updates the availability count, and sends a maintenance ticket to the facility operations team. If three or more sensors on the same floor go offline simultaneously, the system escalates to a potential IoT gateway failure and alerts the infrastructure team to investigate the gateway device. If payment processing fails for more than 1% of transactions in a 5-minute window, the system automatically switches to the backup payment processor and notifies the finance team. These automated runbooks reduce mean time to resolution from hours to minutes for common failure scenarios.

12. Security, Auth & Fraud Prevention

Security in a parking lot system spans multiple domains: physical security (gate access control, surveillance), data security (encryption of PII, PCI compliance for payments), API security (authentication, authorization, rate limiting), and fraud prevention (detecting license plate spoofing, preventing unauthorized free parking, detecting suspicious patterns). The system handles sensitive data including license plate numbers, vehicle images, payment card details, and user location data, all of which must be protected under GDPR, CCPA, and PCI DSS regulations.

Authentication & Authorization

ActorAuth MethodPermissions
Mobile App UserOAuth2 + JWT (RS256)View availability, make reservations, manage payments, view own history
Gate ControllermTLS + API KeySend sensor events, receive gate commands, report status
Facility AdminOAuth2 + RBACManage facility config, view reports, handle disputes, manage pricing
Third-Party APIOAuth2 + Scoped TokensRead availability, create reservations (limited scope)
Internal ServiceService mesh (Istio mTLS)Service-to-service calls within the cluster

Fraud Prevention Rules

C#
public class FraudDetectionService
{
    private readonly IFraudRuleEngine _ruleEngine;
    private readonly IAlertService _alerts;

    public async Task<FraudCheckResult> EvaluateEntryAsync(
        string licensePlate,
        Guid facilityId,
        GeoCoordinate gateLocation)
    {
        var flags = new List<FraudFlag>();

        var recentSessions = await GetRecentSessionsAsync(
            licensePlate, TimeSpan.FromHours(1));

        if (recentSessions.Any(s =>
            s.EntryTime >
            DateTime.UtcNow.AddMinutes(-10)))
        {
            flags.Add(FraudFlag.RapidReentry);
        }

        var blacklisted = await _ruleEngine
            .IsBlacklistedPlateAsync(licensePlate);
        if (blacklisted)
        {
            flags.Add(FraudFlag.BlacklistedVehicle);
        }

        var duplicateAcrossFacilities =
            await CheckDuplicateAcrossFacilitiesAsync(
                licensePlate, facilityId);
        if (duplicateAcrossFacilities)
        {
            flags.Add(FraudFlag.DuplicatePlateDetected);
        }

        var historicalPattern = await _ruleEngine
            .GetUsagePatternAsync(licensePlate);
        if (historicalPattern != null &&
            historicalPattern.AverageDuration <
            TimeSpan.FromMinutes(2) &&
            historicalPattern.TotalVisits > 20)
        {
            flags.Add(FraudFlag.SuspiciousShortStays);
        }

        if (flags.Any())
        {
            await _alerts.RaiseAsync(
                new FraudAlert
                {
                    LicensePlate = licensePlate,
                    FacilityId = facilityId,
                    Flags = flags,
                    Timestamp = DateTime.UtcNow
                });
        }

        return new FraudCheckResult
        {
            IsBlocked = flags.Contains(
                FraudFlag.BlacklistedVehicle) ||
                flags.Contains(
                    FraudFlag.DuplicatePlateDetected),
            RequiresManualReview =
                flags.Count >= 2,
            Flags = flags
        };
    }
}

Data Protection Measures

  • Encryption at Rest: All PII (license plates, user data, payment info) encrypted using AES-256 with AWS KMS or Azure Key Vault managed keys.
  • Encryption in Transit: TLS 1.3 for all API communication, mTLS for service-to-service and IoT device communication.
  • Data Retention: License plate images retained for 30 days for dispute resolution, then permanently deleted. Session data retained for 7 years for tax/accounting compliance.
  • PCI DSS: Payment card data is tokenized via Stripe; the system never stores raw card numbers. PCI scope is minimized by using Stripe Elements for card input.
  • GDPR/CCPA: Users can request data export or deletion. License plate data is treated as PII and subject to right-to-erasure requests (after legal retention period).
  • Rate Limiting: API rate limits of 100 requests/minute per user and 1,000 requests/minute per facility prevent abuse and DDoS attacks.
  • Audit Logging: Every gate operation, payment, admin action, and system event is logged with actor identity, timestamp, and IP address for forensic analysis.

16. Cost Estimation

The cost estimation covers both the one-time hardware setup and the ongoing cloud infrastructure for running the parking lot management system. We provide estimates for a single large facility with 5,000 spots that processes 8,000 transactions per day. Costs vary significantly based on geographic region, hardware vendor negotiations, and cloud provider pricing tiers. The estimates below assume a mid-tier metropolitan area in the United States with Azure cloud infrastructure.

Hardware procurement is the largest upfront investment. Ultrasonic sensors are the most numerous component — one per parking spot — and their cost scales linearly with facility size. Magnetic in-ground sensors are more expensive per unit but offer higher accuracy and longer battery life, making them a better choice for high-traffic commercial facilities where maintenance access is difficult. LPR cameras represent a significant per-unit cost but are essential for the entry/exit automation workflow. Barrier gates must be industrial-grade, rated for millions of open/close cycles, and capable of operating during power outages with battery backup. LED indicators and digital signage provide the visual navigation infrastructure that guides drivers to their assigned spots, reducing congestion and improving the overall user experience.

One-Time Hardware Costs (Per Facility)

ComponentQuantityUnit CostTotal
Ultrasonic Sensors5,000$20$100,000
IoT Gateways (per floor)5$500$2,500
LPR Cameras (entry/exit lanes)8$1,500$12,000
Barrier Gates (entry/exit)8$3,000$24,000
LED Directional Indicators200$50$10,000
Digital Signage Displays20$300$6,000
EV Charging Stations50$2,000$100,000
Networking (switches, cabling)1 lot$15,000$15,000
Edge Servers (per floor)5$2,000$10,000
Total Hardware$279,500

Monthly Cloud Infrastructure Costs

ServiceSpecificationMonthly Cost
Azure/AWS VMs (App Services)4 × D4s_v3 (4 vCPU, 16GB)$560
Azure/AKS Kubernetes Cluster3-node cluster, D4s_v3$840
PostgreSQL (Managed)Standard tier, 2 vCPU, 64GB storage$350
Redis Cache (Managed)Standard C3 (6GB)$300
InfluxDB / TimescaleDBSelf-hosted on VM$150
Apache Kafka (Managed)Basic cluster, 3 brokers$450
MQTT Broker (IoT Hub)Standard tier, 500K messages/day$200
S3 / Blob Storage1TB + CDN$50
Bandwidth (Egress)500GB/month$40
SSL CertificatesWildcard + SAN$0 (Let's Encrypt)
Monitoring (Datadog/Grafana Cloud)Pro tier$200
SMS Notifications (Twilio)10,000 SMS/month$100
Push Notifications (Firebase)Unlimited$0
Payment Processing (Stripe)2.9% + $0.30 per transaction$2,500
Total Monthly Cloud$5,740

Annual Operational Costs

CategoryAnnual Cost
Cloud Infrastructure$68,880
Hardware Maintenance & Replacement (15% of hardware cost)$41,925
IoT Sensor Battery Replacement (annual)$5,000
Software Licenses (monitoring, CI/CD, etc.)$12,000
Security Audits & PCI Compliance Assessment$15,000
Network & Cellular Connectivity$6,000
Insurance (cyber liability + equipment)$8,000
Engineering Team (2 backend + 1 DevOps, fractional allocation)$120,000
Total Annual Operational$276,805
Cost Optimization Strategies: Using spot instances for non-critical workloads (analytics batch processing, log aggregation) can reduce cloud costs by 30-40%. Implementing tiered storage for sensor data — hot storage in InfluxDB for 7 days, warm storage in TimescaleDB for 90 days, cold storage in compressed Parquet files on S3 for historical analytics — further reduces storage costs. Negotiating enterprise agreements with cloud providers for multi-facility deployments can yield 20-25% discounts on compute and managed services.
ROI Analysis: A single valet attendant costs approximately $3,500/month. With 5,000 spots requiring at least 6 attendants across shifts, the manual staffing cost is ~$21,000/month. The automated system at $5,740/month saves over $15,000/month in labor alone, paying for the $279,500 hardware investment in approximately 19 months. Additional savings come from optimized spot utilization (15-20% more revenue from better allocation), reduced theft/loss, and dynamic pricing uplift.

17. Testing Strategy

Given the cyber-physical nature of the parking lot system, testing must cover not only standard software concerns (unit, integration, end-to-end) but also hardware integration, concurrency under load, failure recovery, and financial accuracy. A bug in the payment calculation or a race condition in spot assignment can have real-world consequences — incorrect charges or double-booking of spots.

Test Pyramid

LayerCountFocus AreasTools
Unit Tests~500Fee calculation, scoring algorithm, state machine transitions, fraud rulesxUnit, Moq, FluentAssertions
Integration Tests~100Database queries, Redis caching, Kafka event publishing, MQTT messagingTestcontainers, TestStack.Redis
Contract Tests~50API contracts between mobile app and backend, gate controller protocolPact, WireMock
End-to-End Tests~30Full entry-exit flow, reservation flow, payment failure recoverySelenium, Playwright
Load Tests5 scenariosPeak hour entry/exit, concurrent reservations, WebSocket fan-outk6, Gatling
Chaos Tests8 scenariosKafka broker failure, Redis eviction, network partition, gate controller offlineChaos Monkey, Litmus
Hardware-in-Loop~20Actual sensor readings, gate controller commands, camera LPR accuracyCustom test rig, Raspberry Pi simulator

Critical Unit Tests

C#
public class FeeCalculatorTests
{
    private readonly FeeCalculator _calculator;
    private readonly Mock<IFacilityConfigRepository>
        _configRepo;

    public FeeCalculatorTests()
    {
        _configRepo = new Mock<
            IFacilityConfigRepository>();
        _configRepo.Setup(r =>
            r.GetFacilityAsync(It.IsAny<Guid>()))
            .ReturnsAsync(CreateTestFacility());
        _calculator = new FeeCalculator(
            _configRepo.Object, null);
    }

    [Fact]
    public async Task CalculateFee_2Hours_ReturnsCorrectBaseFee()
    {
        var session = CreateSession(
            entry: new DateTime(2026, 7, 1, 10, 0, 0),
            dayOfWeek: DayOfWeek.Wednesday);
        var exitTime = new DateTime(
            2026, 7, 1, 12, 0, 0);

        var fee = await _calculator
            .CalculateFeeAsync(session, exitTime);

        Assert.Equal(8.00m, fee.BaseFee);
        Assert.Equal(8.00m, fee.TotalDue);
    }

    [Fact]
    public async Task CalculateFee_Weekend_AppliesMultiplier()
    {
        var session = CreateSession(
            entry: new DateTime(2026, 7, 4, 10, 0, 0),
            dayOfWeek: DayOfWeek.Saturday);
        var exitTime = new DateTime(
            2026, 7, 4, 12, 0, 0);

        var fee = await _calculator
            .CalculateFeeAsync(session, exitTime);

        Assert.Equal(8.00m * 1.25m, fee.BaseFee);
    }

    [Fact]
    public async Task CalculateFee_ExceedsDailyMax_CapsAtMaxRate()
    {
        var session = CreateSession(
            entry: new DateTime(2026, 7, 1, 6, 0, 0),
            dayOfWeek: DayOfWeek.Wednesday);
        var exitTime = new DateTime(
            2026, 7, 1, 23, 0, 0);

        var fee = await _calculator
            .CalculateFeeAsync(session, exitTime);

        Assert.Equal(25.00m, fee.BaseFee);
    }

    [Fact]
    public async Task CalculateFee_ReservationApplied_DiscountsFee()
    {
        var session = CreateSession(
            entry: new DateTime(2026, 7, 1, 10, 0, 0),
            reservationId: Guid.NewGuid());
        var exitTime = new DateTime(
            2026, 7, 1, 12, 0, 0);

        var fee = await _calculator
            .CalculateFeeAsync(session, exitTime);

        Assert.True(
            fee.ReservationDiscount > 0);
        Assert.True(
            fee.TotalDue < fee.BaseFee);
    }
}

Concurrency Tests

C#
public class SpotAssignmentConcurrencyTests
{
    [Fact]
    public async Task AssignSpot_TwoVehiclesSameSpot_OneWins()
    {
        var facility = await SetupFacilityAsync(
            availableSpots: 1);
        var service = CreateAssignmentService();

        var task1 = service.AssignSpotAsync(
            facility.Id, SpotType.Regular,
            Guid.NewGuid(), null);
        var task2 = service.AssignSpotAsync(
            facility.Id, SpotType.Regular,
            Guid.NewGuid(), null);

        var results = await Task.WhenAll(task1, task2);

        var successes = results
            .Where(r => r.IsSuccess).ToList();
        var failures = results
            .Where(r => !r.IsSuccess).ToList();

        Assert.Single(successes);
        Assert.Single(failures);
        Assert.NotEqual(
            successes[0].Spot.Id,
            successes[0].Spot.Id);
    }

    [Fact]
    public async Task AssignSpot_ReservationHonored_DuringConcurrentAccess()
    {
        var facility = await SetupFacilityAsync(
            availableSpots: 2);
        var reservation = await CreateReservationAsync(
            facility.Id, SpotType.EVCharging);
        var service = CreateAssignmentService();

        var tasks = Enumerable.Range(0, 10)
            .Select(i => service.AssignSpotAsync(
                facility.Id, SpotType.EVCharging,
                Guid.NewGuid(), null))
            .ToList();

        tasks.Add(service.AssignSpotAsync(
            facility.Id, SpotType.EVCharging,
            Guid.NewGuid(), reservation.Id));

        var results = await Task.WhenAll(tasks);

        var reservationResult = results
            .Single(r =>
                r.ReservationId == reservation.Id);
        Assert.True(reservationResult.IsSuccess);
    }
}

Disaster Recovery & Business Continuity Testing

We conduct quarterly disaster recovery drills that simulate various failure scenarios and validate the system's ability to recover without data loss or extended downtime. These drills include complete database failover to a standby replica in a different Azure region, Kafka cluster recovery from a broker failure, Redis cache rebuild from persistent storage, and IoT gateway failover to backup cellular connections. Each drill is timed and documented, with a target recovery time objective (RTO) of 5 minutes and a recovery point objective (RPO) of zero data loss for billing data and at most 30 seconds of sensor data loss. The results of each drill feed back into our runbook improvements and infrastructure sizing decisions. We also simulate complete facility power outages to verify that the edge devices can operate independently for up to 4 hours on battery backup, maintaining basic gate operations and local session caching until power is restored.

Chaos Engineering Scenarios

ScenarioExpected BehaviorSuccess Criteria
Kafka broker shutdownProducers buffer messages, consumers switch to remaining brokersNo data loss, <30s recovery
Redis node failureCluster promotes replica, services degrade to DB readsAvailability count stale <10s
Network partition (cloud ↔ floor 3)Local gateway handles entries/exits, queues syncNo trapped vehicles
PostgreSQL primary failureAutomatic failover to synchronous replica<15s failover, no billing errors
Stripe API outageFallback to prepaid credits, queue card paymentsVehicles can still exit
Sensor flood (1000 events/sec)Rate limiter at IoT gateway drops low-confidence readsCloud QPS stays within limits
MQTT broker overloadGate controllers use local cache modeBasic gate ops continue
Complete facility network lossAll gateways enter standalone mode4 hours of autonomous operation

18. Interview Q&A

Q1: How would you handle the scenario where the assigned parking spot's sensor reports it as occupied before the vehicle arrives?

Answer: This is a race condition between the spot assignment and the sensor update. When the Assignment Service reserves a spot, it temporarily transitions the spot status to Assigned (a distinct state from Available and Occupied) in both Redis and PostgreSQL. The sensor event processor checks this status — if a spot is in Assigned state and a sensor reports occupied, the system assumes the assigning vehicle has arrived and transitions to Occupied. If the spot remains in Assigned state for more than 15 minutes without sensor confirmation, the assignment is automatically released and the vehicle is flagged for manual review. This 15-minute window is configurable per facility.

Q2: What happens if the gate controller loses network connectivity during a transaction?

Answer: Each gate controller has a local edge device (Raspberry Pi or industrial PC) that runs a lightweight version of the gate control software. It maintains a local cache of active parking sessions (synced every 30 seconds via MQTT with QoS 1). If the cloud connection is lost, the local controller can: (1) validate entry tickets that were pre-generated, (2) allow exit for vehicles whose sessions exist in the local cache with confirmed payment, and (3) queue unrecognized vehicles at the exit for manual attendant assistance. All local operations are persisted to a local SQLite database and synced to the cloud when connectivity is restored, using a last-writer-wins conflict resolution strategy.

Q3: How do you prevent two vehicles from being assigned the same spot simultaneously?

Answer: We use a combination of database-level optimistic locking and Redis-based distributed locks. The Assignment Service first acquires a Redis distributed lock on the target spot ID. Then it executes an UPDATE query with a WHERE clause that checks the spot's current status is Available or Reserved (for reservation honor). The PostgreSQL row-level lock from the UPDATE ensures only one transaction can claim the spot. If the UPDATE affects zero rows (meaning another transaction claimed it first), the assignment retries with the next-best spot from the scored list. The Redis lock prevents thundering herd on the same spot, while the database lock provides the final consistency guarantee.

Q4: How would you scale this system to support 500 parking facilities across a city?

Answer: Each facility operates as an independent deployment with its own set of microservices, database instances, and Redis clusters. A central control plane manages cross-facility concerns like user accounts, corporate billing, city-wide analytics, and dynamic pricing coordination. The databases are sharded by facility ID. The Kafka cluster is federated, with each facility having its own topic partitions. The API gateway routes requests based on facility ID in the URL path, directing traffic to the appropriate regional cluster. For the mobile app, a single backend-for-frontend (BFF) layer aggregates data from multiple facility services for city-wide search and availability views.

Q5: How do you ensure the system accurately tracks vehicles that enter on one ticket but try to exit using a different gate or method?

Answer: Each parking session is linked to a unique vehicle identity (license plate + RFID tag). The LPR camera at the exit gate captures the plate and the system queries for any active session matching that plate at the facility. If the vehicle entered via RFID but the exit uses LPR (or vice versa), the system resolves the identity through a vehicle profile lookup. If the exit plate does not match any active session, the system flags it as an UnknownVehicle event, alerts a human attendant, and holds the gate. This handles scenarios like stolen tickets, plate cloning, and shared vehicles. The vehicle profile serves as the canonical identity link between multiple identification methods.

Q6: Explain the trade-offs between using a relational database vs. an in-memory store for real-time spot status.

Answer: PostgreSQL provides strong consistency, ACID transactions, and complex query capabilities — ideal for billing, reservations, and audit trails. However, it has higher latency (5-20ms per query) and cannot easily push real-time updates to consumers. Redis provides sub-millisecond reads and native pub/sub for real-time broadcasts, making it perfect for the availability dashboard. The trade-off is that Redis is an in-memory store with limited persistence guarantees. We use Redis as the read-through cache for availability queries and PostgreSQL as the source of truth. Writes go to PostgreSQL first, then propagate to Redis. In a failure scenario where Redis is lost, the system can rebuild the cache from PostgreSQL within minutes using a snapshot rebuild procedure. This gives us the best of both worlds: strong consistency for financial operations and low latency for real-time displays.

Q7: How would you design the system to handle dynamic pricing during special events?

Answer: The Dynamic Pricing Service monitors external signals: scheduled events (concerts, sports games) from an events API, historical demand patterns, real-time occupancy levels, and time-of-day factors. A pricing rule engine evaluates these signals and computes surge multipliers per facility, per zone, and per hour. For example, a concert at a nearby arena might trigger a 1.5x surge at adjacent facilities 2 hours before the event and 2x during the event. The pricing rules are versioned and stored in PostgreSQL, with a circuit breaker that caps maximum surge at 3x to prevent price gouging complaints. All price changes are logged for audit and regulatory compliance, and users are shown the current rate before entering the facility.

Q8: How do you test the physical layer of the system without access to real hardware?

Answer: We use a hardware-in-the-loop testing approach. A custom simulator generates MQTT messages that mimic sensor readings, gate status changes, and camera LPR results. The simulator can replay real-world traffic patterns captured from production facilities or generate synthetic scenarios including edge cases like simultaneous arrivals, sensor failures, and partial gate malfunctions. For the gate controller software itself, we run it on a Raspberry Pi connected to LED indicators and a small barrier mechanism in a test lab. This allows us to verify the complete firmware-to-cloud flow including latency measurements. The test harness can simulate network partitions by blocking MQTT messages at configurable intervals, allowing us to validate the offline fallback logic.

© 2026 Ayodhyya. All rights reserved. | System Design Guides for Senior Engineers