system-design46 min read

How to Design a Parking & EV Charging Reservation System — A Senior+ Guide | Ayodhyya

How to Design a Parking & EV Charging Reservation System

Building a Production-Grade Platform — Real-Time Availability, OCPP Chargers, Dynamic Pricing & Energy Management

Senior+ System Design Guide 10,000+ Words 25 Deep-Dive Sections C# · Mermaid · Real-World Case Studies

1. Introduction & The EV Revolution

The global electric vehicle market is projected to surpass 45 million unit sales annually by 2030, up from roughly 14 million in 2024. This explosive growth creates an urgent infrastructure challenge: drivers need reliable, convenient places to park and charge their vehicles. A fragmented landscape of charging networks, parking garages, and third-party apps makes the experience confusing and unreliable. Drivers arrive at chargers to find them occupied, broken, or incompatible with their vehicle. Parking operators struggle with utilization, manual enforcement, and revenue leakage. The solution is a unified platform that combines parking spot management with EV charging reservations — a system that lets drivers reserve a parking spot with an EV charger in advance, know exactly when and how fast they can charge, pay transparently, and navigate to the charger seamlessly.

Designing this system is a fascinating engineering challenge that spans multiple domains: real-time availability tracking for thousands of physical parking spots and chargers, time-slot reservation booking with conflict resolution, communication with hardware via the OCPP (Open Charge Point Protocol), dynamic pricing algorithms that respond to demand, energy billing with time-of-use rates, load balancing across grid capacity constraints, and integration with vehicle telemetry and navigation systems. The system must handle the physical world's messiness — chargers go offline, drivers overstay, parking sensors malfunction, and grid capacity fluctuates throughout the day. This guide walks through every aspect of building such a system, from data modeling to OCPP integration, from dynamic pricing to grid-level load balancing.

Key Insight: A parking and EV charging reservation system is fundamentally a real-time resource allocation problem with physical constraints. Unlike a restaurant reservation (where the resource is a table), here the resource is a physical parking spot with a charger that has specific power output, connector types, and grid capacity limits. The system must bridge the digital world (reservations, payments, pricing) with the physical world (sensors, chargers, vehicles) while maintaining consistency and a smooth user experience.

Real-World Case Studies

CompanySystemScaleKey Innovation
ChargePointCharging network platform70,000+ charging portsOpen protocol support, fleet management dashboard
Tesla SuperchargerProprietary charging network50,000+ connectorsVehicle-integrated reservation, battery preconditioning
SpotHeroParking reservation marketplace30,000+ parking locationsDynamic pricing engine, real-time inventory sync
AParkMe / Pod PointEU charging + parking6,000+ charge pointsRoaming agreements, multi-OCPP backend support
Recharge (Evercharge)Multi-family EV charging1,000+ propertiesLoad management for existing electrical panels

Tesla's approach is particularly instructive: the vehicle itself is the reservation system. When a Tesla navigates to a Supercharger, the car communicates with the charger, preconditions the battery for optimal charging speed, and automatically handles payment via the owner's Tesla account. This tight vehicle-to-infrastructure integration delivers a seamless experience but only works within the Tesla ecosystem. Our system must be open and interoperable — working with any EV through OCPP-compliant chargers and any parking operator through standardized APIs. This openness is essential for the broader EV adoption that the world needs.

2. Functional & Non-Functional Requirements

Functional Requirements

  1. Parking Spot Discovery: Users can search for available parking spots with EV chargers near a location, filter by connector type (CCS, CHAdeMO, Tesla/NACS), power level (Level 2, DC Fast), price, and availability window. Results show real-time availability with a confidence indicator.
  2. Reservation Booking: Users can reserve a parking spot with an EV charger for a specific time slot. The system must prevent double-booking, hold the reservation for a configurable payment window, and support recurring reservations for commuters.
  3. Real-Time Availability: Parking spots and charger status must reflect reality within 10 seconds. The system must handle sensor failures, manual overrides, and offline chargers gracefully.
  4. Charging Session Management: Users can start, monitor, and stop charging sessions. The system tracks energy delivered (kWh), charging rate, estimated time to target charge, and cost in real-time.
  5. Payment Processing: Users are billed based on energy consumed (per kWh), time parked, and any reservation fees. Support time-of-use pricing, demand charges, and loyalty discounts.
  6. Dynamic Pricing: Parking and charging rates adjust based on demand, time of day, grid electricity costs, and charger utilization. Prices are transparent and shown before reservation.
  7. Wayfinding and Guidance: In-app navigation from the street to the exact parking spot and charger, including floor, row, and spot number.
  8. License Plate Recognition: ANPR/LPR cameras automatically identify vehicles entering and exiting, enabling frictionless access and accurate parking duration billing.
  9. Fleet Management: Fleet operators can manage multiple vehicles, set charging preferences, view aggregated billing, and enforce policies.
  10. Waitlist and Queue: When no chargers are available, users can join a virtual queue and receive notifications when a charger becomes available.

Non-Functional Requirements

RequirementTargetRationale
Availability99.99%Charging infrastructure is essential — downtime strands drivers
Real-Time Accuracy< 10 seconds latencyDrivers rely on accurate status to make reservation decisions
Reservation ConsistencyStrong consistency (no double-booking)Two drivers cannot reserve the same spot simultaneously
Charging Throughput100K concurrent sessionsLarge metropolitan area with thousands of chargers
API Latency (P99)< 200msMobile app responsiveness for real-time search and booking
OCPP Latency< 5 seconds end-to-endStart/stop commands must reach chargers promptly
Data Retention7 years billing, 90 days telemetryRegulatory compliance for financial records
Geospatial Query< 100ms for 50km radiusLocation-based search must be fast
PCI CompliancePCI DSS Level 1Handling credit card data requires strict compliance

3. Capacity Estimation

Parking and Charger Inventory

  • Total parking locations: 5,000 (garages, lots, street parking)
  • Average spots per location: 200 (range: 20 street spots to 2,000 garage spots)
  • Total parking spots: 1,000,000
  • Spots with EV chargers: 50,000 (5% penetration, growing rapidly)
  • Charger connectors: 75,000 (many spots have dual connectors)
  • Average charger power: 50 kW DC fast, 22 kW Level 2

Reservation Volume

  • Daily reservations: 200,000
  • Peak hour reservations (5-7 PM): 40,000/hour = ~11 QPS
  • Average reservation duration: 2.5 hours
  • Active concurrent reservations (peak): 50,000
  • Concurrent charging sessions (peak): 30,000

OCPP Traffic

  • Heartbeat messages: 75,000 connectors x 1/30s = 2,500 heartbeats/second
  • Meter values (every 30s during charging): 30,000 sessions x 1/30s = 1,000 updates/second
  • Transaction events (start/stop): ~2,000/minute = ~33/second average, peaks at 200/second
  • Status notifications: 75,000 x 1/300s = 250/second
  • Remote commands (start/stop/unlock): ~500/second peak

Storage

  • Parking spot metadata: 1M spots x 500 bytes = 500 MB
  • Reservation records (7 years): 200K/day x 365 x 7 x 2 KB = ~1 TB
  • Charging telemetry (90 days): 30K sessions x 24 hours x 120 samples/hour x 200 bytes = ~17 TB
  • Payment records (7 years): 200K/day x 365 x 7 x 1 KB = ~500 GB
Critical Insight: The hardest scaling challenge is maintaining real-time accuracy for 75,000 charger connectors across 5,000 locations. Each charger pushes status updates via OCPP, and each reservation must check availability atomically. The solution is a multi-tier caching strategy: an in-memory reservation engine for active booking windows, Redis for near-real-time charger status, and PostgreSQL for durable state. The reservation engine must be strongly consistent (no double-bookings) while the availability display can be eventually consistent (a few seconds of staleness is acceptable for search results).

4. Data Model & Storage Schema

SQL
CREATE TABLE parking_locations (
    location_id     UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name            VARCHAR(255) NOT NULL,
    address         TEXT NOT NULL,
    city            VARCHAR(100) NOT NULL,
    state           VARCHAR(50),
    country         VARCHAR(50) NOT NULL,
    postal_code     VARCHAR(20),
    latitude        DECIMAL(10, 8) NOT NULL,
    longitude       DECIMAL(11, 8) NOT NULL,
    location_type   VARCHAR(30) NOT NULL,
    total_spots     INTEGER NOT NULL,
    ev_charger_spots INTEGER DEFAULT 0,
    operating_hours JSONB,
    amenities       JSONB,
    operator_id     UUID NOT NULL,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_locations_geo
    ON parking_locations USING GIST (
        ll_to_earth(latitude, longitude)
    );

CREATE TABLE parking_spots (
    spot_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    location_id     UUID NOT NULL REFERENCES parking_locations(location_id),
    spot_number     VARCHAR(20) NOT NULL,
    floor           INTEGER,
    row             VARCHAR(10),
    spot_type       VARCHAR(20) NOT NULL,
    has_charger     BOOLEAN DEFAULT FALSE,
    status          VARCHAR(20) NOT NULL DEFAULT 'available',
    sensor_id       VARCHAR(100),
    last_status_change TIMESTAMPTZ,
    UNIQUE(location_id, spot_number)
);

CREATE TABLE ev_chargers (
    charger_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    location_id     UUID NOT NULL REFERENCES parking_locations(location_id),
    spot_id         UUID REFERENCES parking_spots(spot_id),
    charger_name    VARCHAR(100),
    ocpp_charge_point_id VARCHAR(100) NOT NULL UNIQUE,
    model           VARCHAR(100),
    manufacturer    VARCHAR(100),
    power_level     VARCHAR(20) NOT NULL,
    max_power_kw    DECIMAL(6, 2) NOT NULL,
    status          VARCHAR(20) NOT NULL DEFAULT 'available',
    firmware_version VARCHAR(50),
    last_heartbeat  TIMESTAMPTZ,
    last_status_change TIMESTAMPTZ,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE charger_connectors (
    connector_id    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    charger_id      UUID NOT NULL REFERENCES ev_chargers(charger_id),
    connector_number INTEGER NOT NULL,
    connector_type  VARCHAR(20) NOT NULL,
    max_power_kw    DECIMAL(6, 2) NOT NULL,
    status          VARCHAR(20) NOT NULL DEFAULT 'available',
    current_session_id UUID,
    UNIQUE(charger_id, connector_number)
);

CREATE TABLE reservations (
    reservation_id  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL,
    location_id     UUID NOT NULL REFERENCES parking_locations(location_id),
    spot_id         UUID NOT NULL REFERENCES parking_spots(spot_id),
    connector_id    UUID REFERENCES charger_connectors(connector_id),
    vehicle_id      UUID,
    status          VARCHAR(20) NOT NULL DEFAULT 'pending',
    start_time      TIMESTAMPTZ NOT NULL,
    end_time        TIMESTAMPTZ NOT NULL,
    actual_arrival  TIMESTAMPTZ,
    actual_departure TIMESTAMPTZ,
    reservation_fee DECIMAL(10, 2) DEFAULT 0,
    estimated_energy_kwh DECIMAL(8, 2),
    target_soc      INTEGER,
    pricing_tier    VARCHAR(30),
    recurrence      JSONB,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_reservations_user
    ON reservations(user_id, start_time DESC);
CREATE INDEX idx_reservations_spot_time
    ON reservations(spot_id, start_time, end_time)
    WHERE status IN ('confirmed', 'checked_in', 'active_charging');
CREATE INDEX idx_reservations_location_time
    ON reservations(location_id, start_time);

CREATE TABLE charging_sessions (
    session_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    reservation_id  UUID REFERENCES reservations(reservation_id),
    user_id         UUID NOT NULL,
    charger_id      UUID NOT NULL REFERENCES ev_chargers(charger_id),
    connector_id    UUID NOT NULL REFERENCES charger_connectors(connector_id),
    vehicle_id      UUID,
    ocpp_transaction_id VARCHAR(100),
    status          VARCHAR(20) NOT NULL DEFAULT 'starting',
    start_time      TIMESTAMPTZ,
    end_time        TIMESTAMPTZ,
    energy_delivered_kwh DECIMAL(10, 3) DEFAULT 0,
    peak_power_kw   DECIMAL(8, 2),
    average_power_kw DECIMAL(8, 2),
    duration_minutes INTEGER,
    start_soc       INTEGER,
    end_soc         INTEGER,
    cost_breakdown  JSONB,
    total_cost      DECIMAL(10, 2),
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

Storage Strategy

DataStorageRationale
Parking locations and spotsPostgreSQL + PostGISGeospatial queries for location search
Real-time spot availabilityRedis (hot) + PostgreSQL (durable)Sub-second reads for availability checks
ReservationsPostgreSQL (primary) + Redis (lock)ACID for booking consistency
Charger status (OCPP)Redis + TimescaleDBFast status reads, time-series for telemetry
Charging telemetryTimescaleDB (90 days) then S3 ParquetTime-series optimized, archival for analytics
Payment recordsPostgreSQL (encrypted columns)PCI compliance, audit trail
Geospatial indexPostGIS / Redis GEORadius-based search for nearby chargers

5. High-Level Architecture Overview

The system consists of six main layers: the client layer (mobile apps, web dashboard, third-party integrations), the API gateway (authentication, rate limiting, routing), the core services (reservation engine, availability tracker, pricing engine, charging manager), the integration layer (OCPP gateway, payment gateway, ANPR integration, mapping services), the messaging layer (Kafka for event streaming, Redis pub/sub for real-time updates), and the storage layer (PostgreSQL, Redis, TimescaleDB, S3). Each layer is independently scalable and deployable.

graph TB subgraph Clients["Client Layer"] Mobile["Mobile App (iOS/Android)"] Web["Web Dashboard"] Fleet["Fleet Portal"] ThirdParty["Third-Party API"] end subgraph Gateway["API Gateway"] Auth["Authentication / OAuth2"] RateLimit["Rate Limiter"] Router["Request Router"] end subgraph Core["Core Services"] ResEngine["Reservation Engine"] AvailTracker["Availability Tracker"] PricingEngine["Dynamic Pricing Engine"] ChargingMgr["Charging Manager"] BillingSvc["Billing Service"] WaitlistSvc["Waitlist Service"] end subgraph Integration["Integration Layer"] OCPPGW["OCPP Gateway"] PaymentGW["Payment Gateway"] ANPR["ANPR/LPR Service"] MapSvc["Mapping and Navigation"] end subgraph Messaging["Messaging Layer"] Kafka["Kafka (Event Stream)"] RedisPubSub["Redis Pub/Sub"] SignalR["SignalR (WebSocket)"] end subgraph Storage["Storage Layer"] PG["PostgreSQL + PostGIS"] Redis["Redis Cluster"] Timescale["TimescaleDB"] S3["S3 (Archive)"] end Mobile --> Auth Web --> Auth Fleet --> Auth ThirdParty --> Auth Auth --> Router Router --> ResEngine Router --> AvailTracker Router --> PricingEngine Router --> ChargingMgr Router --> BillingSvc ResEngine --> Kafka ChargingMgr --> OCPPGW OCPPGW --> Kafka BillingSvc --> PaymentGW ANPR --> Kafka ResEngine --> PG AvailTracker --> Redis ChargingMgr --> Timescale Kafka --> RedisPubSub RedisPubSub --> SignalR SignalR --> Mobile

Request Flow: Reserve and Charge

  1. Search: User opens the app and searches for chargers near their destination. The API gateway queries the geospatial index for chargers within the specified radius. Results are enriched with real-time availability from Redis, current pricing from the pricing engine, and distance/drive-time estimates from the mapping service.
  2. Reserve: User selects a spot and time slot. The reservation engine acquires a distributed lock on the spot, verifies availability, creates the reservation in PostgreSQL within a transaction, and updates the availability tracker. A confirmation is sent via SignalR/FCM and the user receives a QR code for entry.
  3. Arrive: User arrives at the location. ANPR cameras detect the license plate and match it to the reservation. The gate opens automatically. The reservation transitions from confirmed to checked_in. The user navigates to their assigned spot using in-app wayfinding.
  4. Charge: User plugs in the vehicle. The OCPP gateway detects the cable insertion, sends a RemoteStartTransaction command to the charger, and a charging session begins. Real-time meter values flow from the charger through OCPP to the charging manager, which streams updates to the user's app via SignalR.
  5. Pay and Depart: User stops charging. The billing service calculates the total cost, charges the user's payment method, and generates a receipt. The ANPR camera detects departure, the gate opens, and the reservation transitions to completed.
Architecture Principle: The reservation engine is the single source of truth for spot availability. All availability queries ultimately resolve to what reservations exist for this spot during this time window. This avoids the consistency nightmare of trying to synchronize state across multiple systems. The availability tracker in Redis is a read-optimized cache that is rebuilt from reservation state, not maintained independently.

6. Parking Spot Management and Real-Time Availability

Real-time availability tracking is the foundation of the entire system. If the availability data is wrong, reservations fail, drivers arrive at occupied spots, and trust erodes. The system must handle three sources of availability data: sensor-based detection (ground sensors, cameras), OCPP-reported charger status, and reservation system state. These sources are combined using a priority-based reconciliation algorithm that favors the most recent and most authoritative data source.

Availability Data Sources

SourceLatencyAccuracyCoverage
Parking sensors (ground/ultrasonic)1-3 seconds99.5%Only equipped spots
OCPP charger status5-30 seconds99.9%Charger connectors only
ANPR camera detection3-10 seconds97%Entrance/exit points
Reservation system stateReal-time100% (for reserved spots)Reserved spots only
Manual operator overrideImmediate100%Any spot (maintenance, events)

Reconciliation Engine

C#
public class AvailabilityReconciler
{
    private readonly IRedisCluster _redis;
    private readonly IParkingSpotRepository _spotRepo;

    public async Task UpdateAvailabilityAsync(
        string spotId, AvailabilitySource source,
        SpotStatus reportedStatus, DateTime timestamp)
    {
        var key = $"spot:availability:{spotId}";
        var current = await _redis.HashGetAllAsync(key);

        var currentTimestamp = current.FirstOrDefault(
            f => f.Name == "timestamp");
        var currentSource = current.FirstOrDefault(
            f => f.Name == "source");

        if (currentTimestamp.HasValue &&
            DateTime.Parse(currentTimestamp.Value) > timestamp)
        {
            return; // Newer data already exists, ignore stale update
        }

        var authoritativeStatus = ReconcileStatus(
            source, reportedStatus,
            currentSource.HasValue
                ? Enum.Parse<AvailabilitySource>(currentSource.Value)
                : AvailabilitySource.Initial,
            current.FirstOrDefault(f => f.Name == "status").Value);

        await _redis.HashSetAsync(key, new HashEntry[]
        {
            new("status", authoritativeStatus.ToString()),
            new("source", source.ToString()),
            new("timestamp", timestamp.ToString("O"))
        });
        await _redis.KeyExpireAsync(key, TimeSpan.FromMinutes(5));
    }

    private SpotStatus ReconcileStatus(
        AvailabilitySource newSource, SpotStatus newStatus,
        AvailabilitySource currentSource, string currentStatus)
    {
        if (currentSource == AvailabilitySource.ReservationSystem &&
            Enum.Parse<SpotStatus>(currentStatus) == SpotStatus.Reserved)
            return SpotStatus.Reserved;

        if (newSource == AvailabilitySource.Sensor)
            return newStatus;

        if (newSource == AvailabilitySource.OCPP)
            return newStatus;

        return Enum.Parse<SpotStatus>(currentStatus ?? "unknown");
    }
}
Sensor Failure Handling: When a parking sensor stops reporting (heartbeat missed for 5 minutes), the system falls back to the last known status and marks the spot as sensor_unconfirmed with a reduced confidence score. Search results show this reduced confidence to users. The operations team receives a maintenance alert. After 24 hours without sensor data, the spot is marked as unverified and excluded from reservation bookings until the sensor is repaired.

Availability Cache Architecture

The availability cache in Redis is organized as a nested structure: a GeoSet for location-level indexing (enabling radius-based search), Hash maps per location for spot-level status, and sorted sets for time-range availability queries. The cache is updated via a Kafka consumer that processes availability events from all sources. The consumer runs an event-sourcing model — every status change is an immutable event that is applied to the cache state. This provides a complete audit trail and enables rebuilding the cache from scratch if Redis data is lost.

7. Reservation System and Time-Slot Booking

The reservation system is the core business logic component. It must handle concurrent booking requests, prevent double-booking, manage time-slot conflicts, support various reservation types (one-time, recurring, fleet), and maintain strong consistency while serving at least 11 QPS during peak hours. The key design decision is using a pessimistic locking strategy for the booking path — the system acquires a lock on the spot + time range before checking availability and creating the reservation.

Reservation State Machine

stateDiagram-v2 [*] --> Pending: User submits booking Pending --> Confirmed: Payment authorized Pending --> Cancelled: Payment failed / user cancels Confirmed --> CheckedIn: User arrives Confirmed --> NoShow: Grace period expires 30 min CheckedIn --> ActiveCharging: Cable plugged in CheckedIn --> Expired: Reservation time ends ActiveCharging --> Finishing: User stops Finishing --> Completed: Payment processed Completed --> [*] Cancelled --> [*] NoShow --> [*] Expired --> [*]

Booking Engine

C#
public class ReservationEngine
{
    private readonly IDistributedLock _lockProvider;
    private readonly IParkingSpotRepository _spotRepo;
    private readonly IReservationRepository _reservationRepo;
    private readonly IPricingEngine _pricingEngine;

    public async Task<ReservationResult> CreateReservationAsync(
        CreateReservationRequest request)
    {
        var lockKey = $"booking:{request.SpotId}:" +
            $"{request.StartTime:yyyyMMddHHmm}:" +
            $"{request.EndTime:yyyyMMddHHmm}";

        await using var lockHandle = await _lockProvider
            .AcquireAsync(lockKey, TimeSpan.FromSeconds(30));

        if (lockHandle == null)
            return ReservationResult.Fail(
                "Booking in progress, please retry");

        var spot = await _spotRepo.GetSpotAsync(request.SpotId);
        if (spot == null || !spot.HasCharger)
            return ReservationResult.Fail(
                "Spot not available for reservation");

        var conflicts = await _reservationRepo
            .GetConflictingReservationsAsync(
                request.SpotId, request.StartTime, request.EndTime);

        if (conflicts.Any())
            return ReservationResult.Fail(
                "Time slot no longer available");

        var pricing = await _pricingEngine.CalculatePriceAsync(
            request.LocationId, request.StartTime, request.EndTime,
            request.EstimatedEnergyKwh);

        var reservation = new Reservation
        {
            Id = Guid.NewGuid(),
            UserId = request.UserId,
            LocationId = request.LocationId,
            SpotId = request.SpotId,
            ConnectorId = request.ConnectorId,
            Status = ReservationStatus.Pending,
            StartTime = request.StartTime,
            EndTime = request.EndTime,
            ReservationFee = pricing.ReservationFee,
            EstimatedEnergyKwh = request.EstimatedEnergyKwh,
            TargetSoc = request.TargetSoc,
            PricingTier = pricing.TierName
        };

        await _reservationRepo.CreateAsync(reservation);

        var paymentHeld = await AuthorizePaymentAsync(
            request.UserId, pricing.TotalEstimate);
        if (!paymentHeld)
        {
            reservation.Status = ReservationStatus.Cancelled;
            await _reservationRepo.UpdateAsync(reservation);
            return ReservationResult.Fail(
                "Payment authorization failed");
        }

        reservation.Status = ReservationStatus.Confirmed;
        await _reservationRepo.UpdateAsync(reservation);

        return ReservationResult.Success(reservation);
    }
}

Time-Slot Conflict Detection

The conflict detection query is the performance-critical path. Two intervals overlap if and only if A_start < B_end AND B_start < A_end. The query is optimized with a composite index on (spot_id, start_time, end_time) where status is active.

SQL
SELECT COUNT(*) as conflict_count
FROM reservations
WHERE spot_id = @SpotId
  AND status IN ('confirmed', 'checked_in', 'active_charging')
  AND start_time < @EndTime
  AND end_time > @StartTime;

Recurring Reservations

Commuters often need the same parking spot at the same time every weekday. The system supports recurring reservations defined by RRULE format. When a recurring reservation is created, the system pre-books the next 4 weeks and creates calendar events. Each recurrence is an individual reservation record linked by a recurrence_group_id. The system automatically extends recurring reservations by 2 weeks each week, with an opt-out window of 24 hours before each occurrence.

Recurring Reservation Conflicts: Recurring reservations create a future lock on spots that may conflict with one-time bookings. Our policy: recurring reservations hold priority for their confirmed window, but one-time bookings can fill gaps between recurring slots. If a recurring holder doesn't show up (no-show), their spot is released after the grace period and becomes available for walk-ins.

8. EV Charger Management and OCPP Protocol

The OCPP (Open Charge Point Protocol) is the industry-standard communication protocol between charging stations and the central management system. OCPP 1.6 uses JSON over WebSocket, while OCPP 2.0.1 adds enhanced security, smart charging, and device management features. Our system implements an OCPP gateway that handles the WebSocket connections to all chargers, translates OCPP messages into internal domain events, and sends remote commands back to chargers.

OCPP Message Types

OCPP MessageDirectionPurposeFrequency
HeartbeatCharge Point to CMSKeep-alive, indicates charger is onlineEvery 30s
StatusNotificationCharge Point to CMSCharger status changedOn status change
BootNotificationCharge Point to CMSCharger startup, registrationOn boot
StartTransactionCharge Point to CMSUser initiated charging sessionOn plug-in
StopTransactionCharge Point to CMSCharging session endedOn unplug
MeterValuesCharge Point to CMSEnergy meter readingsEvery 30s during charging
RemoteStartTransactionCMS to Charge PointRemote start charging (app-initiated)On user request
RemoteStopTransactionCMS to Charge PointRemote stop chargingOn user request
UnlockConnectorCMS to Charge PointUnlock stuck cableOn user request
ResetCMS to Charge PointReboot chargerMaintenance

OCPP Gateway Implementation

C#
public class OcppGateway : BackgroundService
{
    private readonly ConcurrentDictionary<string, OcppConnection>
        _connections = new();
    private readonly IKafkaProducer _eventProducer;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        var listener = new HttpListener();
        listener.Prefixes.Add("wss://0.0.0.0:8443/ocpp/");
        listener.Start();

        while (!ct.IsCancellationRequested)
        {
            var context = await listener.GetContextAsync();
            _ = HandleConnectionAsync(context, ct);
        }
    }

    private async Task HandleConnectionAsync(
        HttpListenerContext context, CancellationToken ct)
    {
        var chargePointId = ExtractChargePointId(context.Request);
        var ws = await context.AcceptWebSocketAsync("ocpp1.6");
        var connection = new OcppConnection(
            chargePointId, ws.WebSocket);
        _connections[chargePointId] = connection;

        try
        {
            await foreach (var message in
                connection.ReadMessagesAsync(ct))
            {
                await ProcessMessageAsync(chargePointId, message);
            }
        }
        finally
        {
            _connections.TryRemove(chargePointId, out _);
            await connection.CloseAsync();
        }
    }

    private async Task ProcessMessageAsync(
        string chargePointId, OcppMessage message)
    {
        switch (message.Action)
        {
            case "Heartbeat":
                await _eventProducer.PublishAsync("ocpp.heartbeat",
                    new OcppHeartbeatEvent(
                        chargePointId, DateTime.UtcNow));
                break;
            case "StatusNotification":
                var status = Deserialize<StatusNotificationRequest>(
                    message.Payload);
                await _eventProducer.PublishAsync("ocpp.status",
                    new OcppStatusEvent(chargePointId,
                        status.ConnectorId, status.Status,
                        status.ErrorCode));
                break;
            case "MeterValues":
                var meter = Deserialize<MeterValuesRequest>(
                    message.Payload);
                await _eventProducer.PublishAsync("ocpp.meter",
                    new OcppMeterEvent(chargePointId, meter));
                break;
            case "StartTransaction":
                var startTx = Deserialize<StartTransactionRequest>(
                    message.Payload);
                await _eventProducer.PublishAsync(
                    "ocpp.transaction.start",
                    new OcppTransactionStartEvent(
                        chargePointId, startTx));
                break;
            case "StopTransaction":
                var stopTx = Deserialize<StopTransactionRequest>(
                    message.Payload);
                await _eventProducer.PublishAsync(
                    "ocpp.transaction.stop",
                    new OcppTransactionStopEvent(
                        chargePointId, stopTx));
                break;
        }
    }

    public async Task SendRemoteCommandAsync(
        string chargePointId, OcppMessage command)
    {
        if (_connections.TryGetValue(chargePointId,
            out var connection))
        {
            await connection.SendMessageAsync(command);
        }
        else
        {
            throw new ChargerOfflineException(chargePointId);
        }
    }
}
OCPP 2.0.1 Enhancements: OCPP 2.0.1 introduces SecurityEventNotification for tamper detection, CertificateSigned for TLS certificate management, and Get15118EVCertificate for ISO 15118 Plug and Charge support. It also adds Smart Charging profiles that allow the CMS to set power limits per charger based on grid capacity. Our system supports both OCPP 1.6 and 2.0.1, with automatic protocol detection during the BootNotification handshake.

9. Charger States and Lifecycle

Understanding charger states is critical for both the reservation system (which needs to know if a charger is bookable) and the charging manager (which needs to know if a charger is ready to start a session). Each charger connector has an independent state machine.

Charger State Machine

stateDiagram-v2 [*] --> Available: Boot / Session ends Available --> Reserved: Reservation created Available --> Charging: User plugs in Reserved --> Charging: User arrives plugs in Reserved --> Available: Reservation expires Charging --> Available: Session ends normally Charging --> Suspended: User pauses Suspended --> Charging: User resumes Charging --> Faulted: Hardware error Faulted --> Available: Fault cleared Available --> Offline: Communication lost Offline --> Available: Communication restored Available --> Unavailable: Maintenance scheduled Unavailable --> Available: Maintenance complete

State Definitions and Behaviors

StateReservable?Chargable?User Sees
AvailableYesYesGreen icon, Ready to charge
ReservedNoOnly reservation holderYellow icon, Reserved
ChargingNoNoBlue icon, In use
SuspendedNoResume onlyOrange icon, Paused
FaultedNoNoRed icon, Out of service
OfflineNoNoGray icon, Unavailable
UnavailableNoNoGray icon, Under maintenance

The state machine enforces strict transition rules. A charger in the Faulted state cannot transition directly to Charging — it must first be diagnosed and reset, transitioning through Available. The Suspended state is important for smart charging: when the grid operator requests a power reduction, the charger suspends charging and enters a queue. When power is restored, the charger resumes in the order it was suspended.

Faulted Charger Handling: When a charger enters the Faulted state, the system must immediately: (1) stop any active charging session gracefully, (2) unlock the connector so the user can unplug, (3) exclude the charger from availability, (4) notify users with reservations at that location, (5) create a maintenance ticket, and (6) attempt a remote reset if the fault is transient. If the remote reset fails, the charger remains faulted until a technician visits.

10. Dynamic Pricing and Demand-Based Rates

Dynamic pricing optimizes revenue and utilization by adjusting rates based on real-time demand, grid electricity costs, time of day, and competitive factors. The pricing engine must compute prices for any spot/connector/time combination in under 50ms and display them to the user before they confirm a reservation.

Pricing Factors

FactorImpact on PriceData Source
Time of dayPeak hours +30-50%Historical demand patterns
Grid electricity costWholesale rates vary 5x dailyUtility API / wholesale market
Location demandOver 80% utilization = surgeReal-time charger availability
Charger power levelDC Fast = premium over Level 2Charger configuration
Day of weekWeekends may be +20% in business districtsHistorical patterns
WeatherExtreme cold/heat = +10%Weather API
Special eventsSports/concerts = custom surgeEvent calendar integration
Membership tierPremium members -10-20%User account

Pricing Algorithm

C#
public class DynamicPricingEngine
{
    private readonly IGridCostProvider _gridCostProvider;
    private readonly IDemandAnalyzer _demandAnalyzer;

    public async Task<PriceQuote> CalculatePriceAsync(
        Guid locationId, DateTime startTime, DateTime endTime,
        decimal estimatedEnergyKwh, Guid? userId = null)
    {
        var baseRate = await GetBaseRateAsync(locationId);
        var gridCost = await _gridCostProvider
            .GetElectricityCostAsync(locationId, startTime, endTime);
        var demandMultiplier = await _demandAnalyzer
            .GetDemandMultiplierAsync(locationId, startTime);
        var timeMultiplier = GetTimeOfDayMultiplier(startTime);
        var powerMultiplier = GetPowerLevelMultiplier(
            await GetChargerPowerLevelAsync(locationId));

        var energyRatePerKwh = baseRate.EnergyRate
            * demandMultiplier
            * timeMultiplier
            * powerMultiplier;

        var energyCost = estimatedEnergyKwh * energyRatePerKwh;
        var parkingFee = CalculateParkingFee(
            baseRate, startTime, endTime, demandMultiplier);
        var reservationFee = baseRate.ReservationFee;

        if (userId != null)
        {
            var discount = await GetMembershipDiscountAsync(
                userId.Value);
            energyCost *= (1 - discount);
            parkingFee *= (1 - discount);
        }

        var totalEstimate = energyCost + parkingFee + reservationFee;

        return new PriceQuote
        {
            EnergyRatePerKwh = energyRatePerKwh,
            EstimatedEnergyCost = Math.Round(energyCost, 2),
            ParkingFee = Math.Round(parkingFee, 2),
            ReservationFee = Math.Round(reservationFee, 2),
            TotalEstimate = Math.Round(totalEstimate, 2),
            PricingTier = demandMultiplier > 1.3m ? "surge" :
                          demandMultiplier > 1.1m ? "peak" : "standard",
            PriceGuaranteeMinutes = 15
        };
    }

    private decimal GetTimeOfDayMultiplier(DateTime time)
    {
        var hour = time.Hour;
        return hour switch
        {
            >= 23 or < 6 => 0.7m,   // Off-peak
            >= 6 and < 7 or >= 20 and < 23 => 0.9m,  // Shoulder
            >= 7 and < 9 => 1.4m,   // Peak morning
            >= 17 and < 20 => 1.5m, // Peak evening
            _ => 1.0m                // Standard
        };
    }
}
Price Transparency: Regulators in many jurisdictions require that EV charging prices be clearly displayed before a session starts. Our system always shows: price per kWh (including all surcharges), estimated total cost for the session, any per-minute fees, any idle fees, and the price guarantee window. The user must explicitly accept the price before the reservation is confirmed.

11. Reservation Lifecycle Management

Every reservation follows a well-defined lifecycle from creation to completion. The lifecycle manager orchestrates state transitions, enforces business rules, and triggers side effects at each transition. The lifecycle is event-sourced — every state change produces an event that is logged, enabling complete audit trails.

Key Lifecycle Events

EventTriggerActions
ReservationCreatedBooking confirmedReserve spot, authorize payment, send confirmation
ReservationRemind1 hour before startPush notification with directions and QR code
CheckInDetectedANPR detects vehicleOpen gate, transition to CheckedIn
GracePeriodExpired30 min after start timeIf no check-in: NoShow, release spot
ChargingStartedOCPP StartTransactionTransition to ActiveCharging
ChargingStoppedOCPP StopTransactionTransition to Finishing, calculate bill
DepartureDetectedANPR detects exitTransition to Completed, process payment
ExtensionRequestedUser requests more timeCheck availability, extend, update billing

Lifecycle Manager Implementation

C#
public class ReservationLifecycleManager
{
    private readonly IReservationRepository _repo;
    private readonly IEventBus _eventBus;
    private readonly IAnprService _anprService;

    public async Task HandleCheckInAsync(
        Guid reservationId, string licensePlate,
        DateTime timestamp)
    {
        var reservation = await _repo.GetByIdAsync(reservationId);
        if (reservation == null) return;

        if (reservation.Status != ReservationStatus.Confirmed)
        {
            await _eventBus.PublishAsync(
                "reservation.invalid_checkin",
                new { reservationId,
                      expected = reservation.Status });
            return;
        }

        var vehicle = await _anprService
            .LookupVehicleAsync(licensePlate);
        if (vehicle?.UserId != reservation.UserId)
        {
            await _eventBus.PublishAsync(
                "reservation.wrong_vehicle",
                new { reservationId, licensePlate });
            return;
        }

        reservation.Status = ReservationStatus.CheckedIn;
        reservation.ActualArrival = timestamp;
        await _repo.UpdateAsync(reservation);

        await _eventBus.PublishAsync("reservation.checked_in",
            new ReservationCheckedInEvent
            {
                ReservationId = reservationId,
                SpotId = reservation.SpotId,
                LocationId = reservation.LocationId,
                Timestamp = timestamp
            });

        await _anprService.OpenGateAsync(
            reservation.LocationId, licensePlate);
    }

    public async Task HandleNoShowCheckAsync()
    {
        var expired = await _repo
            .GetExpiredCheckInsAsync(
                DateTime.UtcNow.AddMinutes(-30));

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

            await _eventBus.PublishAsync("reservation.no_show",
                new ReservationNoShowEvent
                {
                    ReservationId = reservation.Id,
                    SpotId = reservation.SpotId,
                    UserId = reservation.UserId,
                    NoShowFee = reservation.ReservationFee * 0.5m
                });
        }
    }
}

12. Payment Processing and Energy Billing

Payment processing in an EV charging system has unique requirements: the final amount is unknown at the start of the session, sessions can last hours, and billing must be accurate to the kilowatt-hour. The system uses a two-phase payment model: authorization (hold) at reservation time, capture (charge) at session completion.

Billing Components

ComponentUnitExample RateNotes
Energy consumptionper kWh$0.35/kWhBased on metered energy from OCPP
Parking timeper minute$0.10/min ($6/hr)From check-in to check-out
Reservation feeflat$2.00Charged at booking
Idle feeper min after charge$0.25/minIncentivizes prompt removal
Time-of-use premiummultiplier1.0x - 2.0xApplied during peak hours
Processing feeper transaction$0.30Payment processing

Payment Flow

sequenceDiagram participant User participant API participant Billing participant Stripe participant OCPP User->>API: Reserve spot + time slot API->>Billing: Calculate estimate and authorize Billing->>Stripe: Create PaymentIntent (hold) Stripe-->>Billing: Authorization confirmed Billing-->>API: Reservation confirmed API-->>User: Confirmation + QR code Note over User,OCPP: User arrives, plugs in vehicle OCPP->>Billing: StartTransaction event Billing->>Billing: Start metering loop Every 30 seconds OCPP->>Billing: MeterValues (kWh, power) Billing->>API: Real-time cost update API-->>User: Live cost display end OCPP->>Billing: StopTransaction event Billing->>Billing: Calculate final amount Billing->>Stripe: Capture payment (final amount) Stripe-->>Billing: Payment captured Billing-->>API: Invoice generated API-->>User: Receipt + invoice

Billing Service Implementation

C#
public class BillingService
{
    private readonly IPaymentGateway _paymentGateway;
    private readonly IChargingSessionRepository _sessionRepo;
    private readonly IPricingEngine _pricingEngine;

    public async Task<Invoice> FinalizeSessionAsync(Guid sessionId)
    {
        var session = await _sessionRepo.GetByIdAsync(sessionId);
        var pricing = await _pricingEngine
            .GetPricingTierAsync(session.LocationId, session.StartTime);

        var energyCost = session.EnergyDeliveredKwh
            * pricing.EnergyRatePerKwh;

        var parkingMinutes = (int)(session.EndTime - session.StartTime)
            .TotalMinutes;
        var parkingCost = parkingMinutes
            * pricing.ParkingRatePerMinute;

        var idleMinutes = CalculateIdleTime(session);
        var idleCost = idleMinutes * pricing.IdleFeePerMinute;

        var reservationFee = session.ReservationFee;
        var subtotal = energyCost + parkingCost
            + idleCost + reservationFee;
        var tax = subtotal * pricing.TaxRate;
        var total = subtotal + tax;

        var invoice = new Invoice
        {
            Id = Guid.NewGuid(),
            SessionId = sessionId,
            UserId = session.UserId,
            LineItems = new List<InvoiceLineItem>
            {
                new("Energy",
                    $"{session.EnergyDeliveredKwh:F3} kWh",
                    energyCost),
                new("Parking",
                    $"{parkingMinutes} min",
                    parkingCost),
                new("Idle Fee",
                    $"{idleMinutes} min",
                    idleCost),
                new("Reservation", "Flat fee",
                    reservationFee)
            },
            Subtotal = subtotal,
            Tax = tax,
            Total = total,
            Currency = "USD"
        };

        var captureResult = await _paymentGateway
            .CapturePaymentAsync(
                session.PaymentIntentId, total);

        if (captureResult.Succeeded)
            invoice.Status = InvoiceStatus.Paid;

        return invoice;
    }
}
Time-of-Use Billing: If a charging session spans multiple rate periods, the energy consumption must be apportioned to each rate period. OCPP meter values are timestamped, enabling per-interval billing. The billing service groups meter readings by rate period and applies the appropriate rate to each interval's energy consumption.

13. Charging Session Management

A charging session is the core operational unit — the period during which a vehicle is actively receiving energy from a charger. The session manager orchestrates the entire lifecycle from cable insertion to removal, including OCPP communication, real-time telemetry streaming, session monitoring, and coordination with the billing service.

Session Data Stream

During a charging session, the charger transmits meter values every 30 seconds via OCPP. Each meter value contains: energy delivered (kWh cumulative), instantaneous power (kW), voltage (V), current (A), and optionally battery SOC if the vehicle communicates it via ISO 15118. The session manager stores these readings in TimescaleDB for real-time visualization and historical analysis.

C#
public class ChargingSessionManager
{
    private readonly IOcppGateway _ocppGateway;
    private readonly IChargingSessionRepository _sessionRepo;
    private readonly ITimescaleWriter _telemetryWriter;
    private readonly ISignalRHub _realtimeHub;

    public async Task HandleMeterValuesAsync(
        string chargePointId, MeterValuesRequest request)
    {
        var session = await _sessionRepo
            .GetActiveSessionByChargerAsync(chargePointId);
        if (session == null) return;

        var meterReading = new MeterReading
        {
            SessionId = session.Id,
            Timestamp = DateTime.UtcNow,
            EnergyKwh = request.MeterValue.energy,
            PowerKw = request.MeterValue.power,
            Voltage = request.MeterValue.voltage,
            Current = request.MeterValue.current,
            Soc = request.MeterValue.soc
        };

        await _telemetryWriter.InsertAsync(meterReading);

        session.EnergyDeliveredKwh = meterReading.EnergyKwh;
        session.PeakPowerKw = Math.Max(
            session.PeakPowerKw ?? 0, meterReading.PowerKw);

        var currentCost = await CalculateRealTimeCostAsync(
            session, meterReading);
        session.CurrentCost = currentCost;

        await _sessionRepo.UpdateAsync(session);

        await _realtimeHub.SendToUserAsync(session.UserId,
            "charging.update", new
            {
                sessionId = session.Id,
                energyKwh = meterReading.EnergyKwh,
                powerKw = meterReading.PowerKw,
                soc = meterReading.Soc,
                cost = currentCost,
                estimatedMinutesRemaining = CalculateEta(
                    session, meterReading)
            });
    }

    public async Task HandleChargingStallAsync(Guid sessionId)
    {
        var session = await _sessionRepo.GetByIdAsync(sessionId);
        var recentReadings = await _telemetryWriter
            .GetRecentReadingsAsync(
                sessionId, TimeSpan.FromMinutes(5));

        var isStalled = recentReadings.All(
            r => r.PowerKw < 1.0m) &&
            recentReadings.Count >= 10;

        if (isStalled &&
            session.Status == "charging")
        {
            session.Status = "suspended";
            session.SuspensionReason = "stall_detected";
            await _sessionRepo.UpdateAsync(session);

            await _realtimeHub.SendToUserAsync(
                session.UserId,
                "charging.stalled", new
                {
                    sessionId = session.Id,
                    message = "Charging appears to have " +
                        "stopped. Please check your vehicle."
                });
        }
    }
}
Session Resilience: If the network connection between the charger and the CMS is lost during a charging session, the charger continues charging (OCPP spec requires this). When the connection is restored, the charger sends a backlog of missed meter values and a StopTransaction with the final meter values. The session manager reconciles the gap and ensures billing is based on the charger's own meter (which is authoritative).

14. Waitlist and Queue Management

When all chargers at a location are occupied, the waitlist system manages a virtual queue that notifies users when a charger becomes available. The queue must be fair (first-come-first-served), smart (matching connector type to the user's vehicle), and efficient (minimizing time between charger availability and next user charging).

Queue Priority Rules

PriorityConditionBehavior
1 (Highest)Active reservation, on-timeImmediate assignment when spot opens
2Waitlist, compatible vehicleNotified when compatible charger opens
3Waitlist, any connector typeNotified when any charger opens
4 (Lowest)No vehicle detectedLow priority, approximate wait time
C#
public class WaitlistManager
{
    private readonly IWaitlistRepository _waitlistRepo;
    private readonly INotificationService _notifications;

    public async Task NotifyWaitlistForChargerAsync(
        Guid locationId, Guid chargerId,
        string connectorType)
    {
        var waitlist = await _waitlistRepo
            .GetWaitlistAsync(locationId);

        foreach (var entry in waitlist)
        {
            var isCompatible =
                await CheckConnectorCompatibilityAsync(
                    entry.VehicleId, connectorType);

            if (!isCompatible &&
                entry.RequiresSpecificConnector)
                continue;

            var estimatedWait =
                await EstimateWaitTimeAsync(
                    locationId, entry.Position);

            await _notifications.SendAsync(entry.UserId,
                new ChargerAvailableNotification
                {
                    LocationId = locationId,
                    ChargerId = chargerId,
                    ConnectorType = connectorType,
                    EstimatedWaitMinutes = estimatedWait,
                    ExpiresInMinutes = 10,
                    ActionUrl = $"app://charge/{chargerId}"
                });

            entry.Status = WaitlistEntryStatus.Notified;
            entry.NotifiedAt = DateTime.UtcNow;
            entry.ExpiresAt = DateTime.UtcNow.AddMinutes(10);
            await _waitlistRepo.UpdateAsync(entry);

            break; // Only notify the first compatible entry
        }
    }

    public async Task<WaitTimeEstimate> EstimateWaitTimeAsync(
        Guid locationId, int position)
    {
        var activeSessions = await _sessionRepo
            .GetActiveSessionsAsync(locationId);
        var avgRemaining = activeSessions
            .Average(s => (s.ExpectedEndTime - DateTime.UtcNow)
                .TotalMinutes);

        var estimated = position *
            (avgRemaining / activeSessions.Count);

        return new WaitTimeEstimate
        {
            EstimatedMinutes = (int)estimated,
            Confidence = "high"
        };
    }
}
No-Show Fee for Waitlist: When a waitlist entry is notified that a charger is available, they have 10 minutes to arrive and start charging. If they don't respond, they are marked as a no-show and moved to the back of the queue. Three no-shows within 30 days result in a temporary waitlist suspension. This prevents users from holding queue positions without intending to charge.

15. Parking Guidance and Wayfinding

Large parking garages can be confusing, especially for first-time visitors trying to find a specific charger. The wayfinding system provides turn-by-turn navigation from the street entrance to the exact parking spot and charger. In multi-floor garages, the system guides users through ramps and levels.

Wayfinding Architecture

Each parking location has a floor plan stored as a vector map with annotated hotspot positions for every spot, charger, entrance, exit, elevator, and stairwell. The navigation engine computes the shortest path from the user's current GPS location to the reserved spot, considering the garage layout.

C#
public class WayfindingService
{
    private readonly IFloorPlanRepository _floorPlanRepo;

    public async Task<NavigationRoute> GetRouteAsync(
        Guid locationId, Guid targetSpotId,
        GeoPoint? userLocation = null)
    {
        var floorPlan = await _floorPlanRepo
            .GetFloorPlanAsync(locationId, targetSpotId);
        var targetSpot = floorPlan.GetSpot(targetSpotId);

        var startPoint = userLocation ??
            floorPlan.GetNearestEntrance(targetSpot.Position);

        var path = ComputeShortestPath(
            floorPlan.Graph, startPoint, targetSpot.Position);

        var instructions = GenerateInstructions(
            path, floorPlan);

        return new NavigationRoute
        {
            LocationId = locationId,
            TargetSpot = targetSpot,
            Instructions = instructions,
            EstimatedWalkTimeSeconds = CalculateWalkTime(path),
            FloorPlanUrl = floorPlan.ImageUrl,
            PathCoordinates = path
                .Select(p => p.ToGeoJson()).ToList(),
            ChargerInfo = targetSpot.HasCharger ?
                await GetChargerInfoAsync(
                    targetSpot.ChargerId) : null
        };
    }
}

For locations with Bluetooth Low Energy (BLE) beacons, the wayfinding system can provide indoor positioning with 1-3 meter accuracy, enabling turn-by-turn directions even inside concrete structures where GPS fails. The beacons are installed at regular intervals (every 10-15 meters) and the user's phone triangulates its position based on beacon signal strength.

Vehicle Navigation Integration: For vehicles that support the Open Charge Alliance navigation API, the system can push the exact parking spot coordinates directly to the vehicle's built-in navigation system. The driver simply selects the reservation in their car's infotainment system and the car navigates to the spot.

16. License Plate Recognition (ANPR/LPR)

Automatic Number Plate Recognition enables frictionless entry/exit at parking facilities, automatic reservation matching, and accurate parking duration tracking. Cameras installed at entry and exit points capture license plate images, run OCR using machine learning models, and match the recognized plate against active reservations.

ANPR Pipeline

graph LR Camera["Camera Feed"] --> Detect["Plate Detection (YOLO)"] Detect --> Crop["Plate Cropping"] Crop --> OCR["OCR Recognition (CRNN)"] OCR --> Normalize["Plate Normalization"] Normalize --> Match["Reservation Matching"] Match --> Gate["Gate Control"] Match --> Billing["Parking Duration"]
C#
public class AnprService
{
    private readonly IAnprModelRunner _modelRunner;
    private readonly IReservationLookupService _lookup;
    private readonly IGateController _gateController;

    public async Task<AnprResult> ProcessCaptureAsync(
        string locationId, byte[] image, string cameraId)
    {
        var plateRegion = await _modelRunner
            .DetectPlateAsync(image);
        if (plateRegion == null)
            return AnprResult.NoPlateDetected();

        var rawText = await _modelRunner
            .RecognizePlateAsync(plateRegion.CroppedImage);

        var normalized = NormalizePlate(rawText);

        var reservation = await _lookup
            .FindByPlateAsync(normalized, locationId);

        await _eventProducer.PublishAsync("anpr.capture", new
        {
            locationId, cameraId,
            plateText = normalized,
            confidence = plateRegion.Confidence,
            reservationId = reservation?.Id,
            timestamp = DateTime.UtcNow
        });

        if (reservation != null)
        {
            await _gateController.OpenGateAsync(
                locationId, cameraId);
            return AnprResult.Matched(
                reservation, normalized);
        }
        else
        {
            var walkInAllowed =
                await CheckWalkInPolicy(locationId);
            if (walkInAllowed)
            {
                await _gateController.OpenGateAsync(
                    locationId, cameraId);
                return AnprResult.WalkIn(normalized);
            }
            return AnprResult.Rejected(normalized);
        }
    }

    private string NormalizePlate(string raw)
    {
        return raw.Trim().ToUpper()
            .Replace(" ", "").Replace("-", "");
    }
}

The ANPR system achieves 97-99% accuracy in good conditions. For low-confidence reads (below 85%), the system requests a secondary capture from a second camera or falls back to manual verification. The system maintains a plate recognition log with images, confidence scores, and match results for quality monitoring and model retraining.

Privacy Considerations: ANPR systems capture license plates, which are personally identifiable information. The system must comply with local privacy regulations (GDPR in Europe, CCPA in California). License plate images are encrypted at rest, retained for a configurable period (default 30 days), and accessible only to authorized personnel. The system supports privacy mode where plates are hashed for restricted locations.

17. Parking Enforcement

Enforcement ensures that parking rules are followed: reservation holders occupy their reserved spots, non-reservation vehicles don't park in EV-only spots, and vehicles don't overstay. The enforcement system combines automated detection with human operators.

Enforcement Rules

ViolationDetectionResponse
Wrong vehicle in reserved spotANPR mismatchAlert user, notify enforcement
Non-EV in EV charging spotANPR + vehicle databaseFine + vehicle must move
Overstaying reservationSensor + reservation end timeIdle fee charged, spot released
ICEing (blocking charger)Sensor + manual reportFine + towing after warning
Charging complete not movedOCPP meter + idle thresholdIdle fee per minute

The idle fee mechanism is particularly important. After a charging session completes, the system starts an idle timer. The user receives notifications at 5, 15, and 25 minutes after charging completes. After 30 minutes, idle fees of $0.25/minute begin. After 60 minutes, the fee increases to $0.50/minute. This graduated fee structure incentivizes prompt removal while giving users reasonable time to wrap up their visit.

Fair Enforcement Policy: Automated enforcement must be complemented by a fair appeal process. Users can contest enforcement actions through the app with photo evidence (e.g., the charger was faulted). The appeals system uses a three-tier process: automated review, human review, and arbitration. A user-friendly enforcement system builds trust.

18. EV Route Planning and Charger Compatibility

EV drivers planning long trips need to know where they can charge along their route, factoring in their vehicle's range, connector type, and charging speed preferences. The route planner integrates with mapping services to suggest optimal charging stops that minimize total trip time.

Charger Compatibility Matrix

Vehicle TypeNative ConnectorCompatible WithMax Rate
Tesla Model 3/Y/S/XNACS (Tesla)NACS, CCS (adapter)250 kW
CHEVROLET Bolt / Ford Mach-ECCS1CCS1, NACS (2025+)150 kW
Nissan LeafCHAdeMOCHAdeMO, CCS (adapter)50-100 kW
BMW iX / Mercedes EQSCCS2 (EU)CCS2, Type 2 (AC)150-200 kW
Rivian R1T/R1SCCS1CCS1, NACS (adapter)220 kW

Route Planning Algorithm

C#
public class EvRoutePlanner
{
    private readonly IChargerSearchService _chargerSearch;
    private readonly IVehicleProfileService _vehicleProfiles;

    public async Task<RoutePlan> PlanRouteAsync(
        GeoPoint origin, GeoPoint destination,
        Guid vehicleId, DateTime departureTime)
    {
        var vehicle = await _vehicleProfiles.GetAsync(vehicleId);
        var currentRangeKm = vehicle.BatteryCapacityKwh
            * vehicle.EfficiencyKwhPerKm
            * (vehicle.CurrentSoc / 100.0);

        var baseRoute = await GetDrivingRouteAsync(
            origin, destination);

        if (currentRangeKm >
            baseRoute.DistanceKm * 0.85)
        {
            return new RoutePlan
            {
                Stops = new List<RouteStop>(),
                TotalChargingTime = TimeSpan.Zero,
                TotalDetourTime = TimeSpan.Zero
            };
        }

        var stops = await FindOptimalChargingStopsAsync(
            baseRoute, vehicle, currentRangeKm,
            departureTime);

        return new RoutePlan
        {
            Stops = stops,
            TotalChargingTime = stops.Aggregate(
                TimeSpan.Zero,
                (acc, s) => acc + s.EstimatedChargingTime),
            TotalDetourTime = stops.Aggregate(
                TimeSpan.Zero,
                (acc, s) => acc + s.DetourTime)
        };
    }

    private async Task<List<RouteStop>>
        FindOptimalChargingStopsAsync(
            DrivingRoute route, VehicleProfile vehicle,
            double currentRangeKm, DateTime departureTime)
    {
        var stops = new List<RouteStop>();
        var remainingRange = currentRangeKm;
        var currentPosition = route.Origin;
        var currentTime = departureTime;

        while (remainingRange <
            route.DistanceToDestination(
                currentPosition) * 0.85)
        {
            var candidates = await _chargerSearch
                .SearchAlongRouteAsync(
                    route, currentPosition,
                    remainingRange * 0.8,
                    vehicle.CompatibleConnectors);

            if (!candidates.Any())
                return null;

            var best = candidates
                .Select(c => new
                {
                    Charger = c,
                    Score = ScoreChargingStop(
                        c, route, currentTime, vehicle)
                })
                .OrderBy(x => x.Score)
                .First();

            var chargeTime = EstimateChargeTime(
                vehicle, best.Charger, 80);

            stops.Add(new RouteStop
            {
                Charger = best.Charger,
                DetourTime = CalculateDetourTime(
                    route, currentPosition,
                    best.Charger.Location),
                EstimatedChargingTime = chargeTime,
                EstimatedCost = EstimateCost(
                    best.Charger, vehicle, chargeTime)
            });

            remainingRange = vehicle.BatteryCapacityKwh
                * vehicle.EfficiencyKwhPerKm * 0.80;
            currentPosition = best.Charger.Location;
        }

        return stops;
    }
}
Real-Time Route Updates: The route planner continuously monitors charger availability along the route and suggests alternatives if a planned stop becomes unavailable. If the driver is 30 minutes from a planned stop and that charger goes offline, the system immediately recalculates and suggests the next best option. This proactive rerouting is critical for long trips.

19. Load Balancing and Grid Capacity Management

Every parking location has a maximum electrical capacity determined by its utility connection. A typical Level 2 charger draws 7-22 kW, and a DC fast charger draws 50-350 kW. A parking garage with 50 DC fast chargers would need 2.5-17.5 MW of capacity — far more than most buildings have. Load balancing dynamically distributes available power across active charging sessions to stay within grid limits while maximizing the number of vehicles being charged.

Load Balancing Architecture

graph TB subgraph Grid["Utility Grid Connection"] Meter["Smart Meter"] end subgraph Panel["Electrical Panel"] MainBreaker["Main Breaker"] end subgraph LoadMgr["Load Manager"] Controller["Central Controller"] Priority["Priority Engine"] end subgraph Chargers["Charger Group"] C1["Charger 1 50kW"] C2["Charger 2 150kW"] C3["Charger 3 50kW"] C4["Charger 4 150kW"] end Meter --> Controller MainBreaker --> Controller Controller --> Priority Priority --> C1 Priority --> C2 Priority --> C3 Priority --> C4

Load Balancing Algorithm

C#
public class LoadBalancer : BackgroundService
{
    private const decimal SafetyMargin = 0.90m;

    protected override async Task ExecuteAsync(
        CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            foreach (var location in
                await GetManagedLocationsAsync())
            {
                await BalanceLocationAsync(location);
            }
            await Task.Delay(
                TimeSpan.FromSeconds(30), ct);
        }
    }

    private async Task BalanceLocationAsync(
        Location location)
    {
        var maxCapacityKw =
            location.UtilityConnection.MaxCapacityKw;
        var currentDraw = await _gridMonitor
            .GetCurrentDrawAsync(location.Id);
        var availableCapacity =
            (maxCapacityKw * SafetyMargin) - currentDraw;

        var activeChargers = await _chargerRegistry
            .GetActiveChargersAsync(location.Id);

        var prioritized = _priorityEngine
            .Prioritize(activeChargers);

        var allocatedPower = 0m;

        foreach (var charger in prioritized)
        {
            var allocation = Math.Min(
                charger.RequestedPowerKw,
                availableCapacity - allocatedPower);

            if (allocation < charger.MinPowerKw)
            {
                await SuspendChargerAsync(charger,
                    InsufficientCapacity);
                continue;
            }

            if (allocation < charger.RequestedPowerKw)
            {
                await ThrottleChargerAsync(
                    charger, allocation);
            }

            allocatedPower += allocation;
        }
    }
}

public class PriorityEngine
{
    public IReadOnlyList<ActiveCharger> Prioritize(
        IReadOnlyList<ActiveCharger> chargers)
    {
        return chargers
            .OrderByDescending(c => GetPriorityScore(c))
            .ToList();
    }

    private decimal GetPriorityScore(ActiveCharger charger)
    {
        var score = 0m;
        score += (charger.CurrentSoc ?? 50) * 0.3m;
        score += charger.MaxPowerKw * 0.2m;

        if (charger.ReservationEndTime.HasValue)
        {
            var remaining = (charger.ReservationEndTime.Value
                - DateTime.UtcNow).TotalMinutes;
            if (remaining < 30) score += 50;
        }

        if (charger.IsFleetVehicle) score += 20;
        return score;
    }
}

Load Management Strategies

StrategyHow It WorksBest For
Static Load BalancingFixed maximum power per chargerSmall locations
Dynamic Load BalancingPower distributed by demand/priorityMedium locations
Smart Charging (OCPP Profile)CMS sends charging profiles via OCPPLarge locations
V2G (Vehicle-to-Grid)Vehicles discharge back to gridFuture-ready locations
Grid Capacity Planning: When a new parking location is onboarded, the system performs a grid capacity analysis: determines utility connection capacity, calculates maximum chargers supported simultaneously, and designs a load management strategy. The analysis considers coincidence factor (not all chargers at full power simultaneously) and the typical charging curve (power decreases as SOC increases).

20. Demand Response and Fleet Charging

Demand response programs allow utilities to request temporary reductions in electricity consumption during grid stress events. EV charging is an ideal demand response resource because it is deferrable — most vehicles don't need to be charged immediately and can wait 30-60 minutes.

Fleet Charging Management

Fleet operators have unique charging needs: they must charge many vehicles overnight, have strict readiness deadlines (vehicles must be at 100% by 6 AM), and want to minimize electricity cost. The fleet charging manager optimizes charging schedules across the fleet to minimize cost while meeting readiness requirements.

C#
public class FleetChargingOptimizer
{
    public async Task<ChargingSchedule> OptimizeFleetAsync(
        FleetChargingRequest request)
    {
        var schedule = new ChargingSchedule();
        var offPeakStart = GetOffPeakStart(request.Location);
        var offPeakEnd = GetOffPeakEnd(request.Location);

        var sortedVehicles = request.Vehicles
            .OrderBy(v => v.CurrentSoc)
            .ToList();

        var availableHours =
            (offPeakEnd - offPeakStart).TotalHours;

        foreach (var vehicle in sortedVehicles)
        {
            var kwhNeeded =
                (vehicle.TargetSoc - vehicle.CurrentSoc)
                / 100.0 * vehicle.BatteryCapacityKwh;
            var hoursNeeded =
                kwhNeeded / vehicle.ChargerPowerKw;

            if (hoursNeeded <= availableHours)
            {
                schedule.AddEntry(new ChargingEntry
                {
                    VehicleId = vehicle.Id,
                    ScheduledStart = offPeakStart,
                    ScheduledEnd =
                        offPeakStart.AddHours(hoursNeeded),
                    ElectricityRate = "off-peak",
                    EstimatedCost = kwhNeeded
                        * request.OffPeakRate
                });
                availableHours -= hoursNeeded;
            }
            else
            {
                var peakHoursNeeded =
                    hoursNeeded - availableHours;
                schedule.AddEntry(new ChargingEntry
                {
                    VehicleId = vehicle.Id,
                    ScheduledStart = offPeakStart,
                    ScheduledEnd = offPeakEnd,
                    ElectricityRate = "off-peak"
                });
                schedule.AddEntry(new ChargingEntry
                {
                    VehicleId = vehicle.Id,
                    StartPower =
                        vehicle.ChargerPowerKw * 0.7m,
                    ScheduledStart = offPeakEnd,
                    ScheduledEnd =
                        offPeakEnd.AddHours(
                            peakHoursNeeded),
                    ElectricityRate = "peak"
                });
                availableHours = 0;
            }
        }

        schedule.TotalEstimatedCost =
            schedule.Entries.Sum(e => e.EstimatedCost);
        return schedule;
    }
}

Demand Response Integration

The system integrates with utility demand response programs via OpenADR protocol. When the utility sends a demand response event (e.g., reduce load by 200 kW for 2 hours), the load manager automatically reduces charging power across active sessions. Vehicles with higher SOC get more aggressive reduction (they are almost done), while vehicles with lower SOC get less reduction.

Demand Response Revenue: Participating in demand response programs generates revenue for parking operators — typically $50-150/kW-year for capacity commitments. A location with 1 MW of interruptible load can earn $50,000-150,000 per year, significantly offsetting infrastructure costs.

21. Reservation Extensions and No-Show Policies

Real-world parking behavior is messy: users arrive late, leave early, stay longer than planned, or don't show up at all. The system must handle all these cases gracefully while maintaining availability for other users.

Extension Rules

ScenarioRuleFee
Extend before arrivalAvailable if spot free after end timeReservation fee for new period
Extend during chargingAvailable if spot not reservedEnergy + parking for new period
Extend after reservation end15 min grace, then idle feesIdle fee + reservation fee
Auto-extension (charging)If charging over 80% at end, auto-extend 30 minNormal rates

No-Show Policy

C#
public class NoShowPolicy
{
    private readonly TimeSpan _gracePeriod =
        TimeSpan.FromMinutes(30);
    private readonly decimal _noShowFeeRate = 0.5m;

    public async Task HandleNoShowAsync(Reservation reservation)
    {
        if (reservation.Status !=
            ReservationStatus.Confirmed)
            return;

        if (reservation.ActualArrival != null)
            return;

        var graceEnd =
            reservation.StartTime.Add(_gracePeriod);
        if (DateTime.UtcNow < graceEnd)
            return;

        reservation.Status = ReservationStatus.NoShow;
        await _repo.UpdateAsync(reservation);

        var noShowFee =
            reservation.ReservationFee * _noShowFeeRate;

        await _availabilityTracker.ReleaseSpotAsync(
            reservation.SpotId,
            reservation.StartTime,
            reservation.EndTime);

        await _notifications.SendAsync(
            reservation.UserId,
            new NoShowNotification
            {
                ReservationId = reservation.Id,
                NoShowFee = noShowFee,
                Message = "You did not arrive for your " +
                    "reservation. A no-show fee has been " +
                    "applied. The spot has been released."
            });

        await _billingService.ChargeNoShowFeeAsync(
            reservation.UserId, noShowFee,
            reservation.Id);

        await _userService
            .IncrementNoShowCountAsync(
                reservation.UserId);

        var userNoShows = await _userService
            .GetNoShowCountAsync(
                reservation.UserId,
                TimeSpan.FromDays(30));
        if (userNoShows >= 3)
        {
            await _userService
                .SuspendBookingPrivilegeAsync(
                    reservation.UserId,
                    suspensionDays: 7,
                    reason: "3 no-shows in 30 days");
        }
    }
}
No-Show Prevention: The best no-show policy is prevention. The system sends reminders at 24 hours, 2 hours, and 30 minutes before the reservation. Each reminder includes a prominent Cancel button. Users who cancel more than 1 hour before receive a full refund. This cancel-friendly policy reduces no-shows by 40% compared to strict non-refundable policies.

Early Departure Handling

If a user departs before their reservation end time, the system processes an immediate checkout: the billing service calculates the final amount based on actual parking duration and energy consumed (not the reserved duration), and the unused portion of the reservation is refunded (minus a small processing fee). The spot is immediately released back to availability.

22. Monitoring, Security and Compliance

Monitoring and Observability

The system generates massive volumes of operational data: OCPP telemetry from 75,000 connectors, reservation lifecycle events, ANPR captures, payment transactions, and user activity. We use a three-tier monitoring approach: infrastructure metrics, application metrics, and business metrics.

Key Business Metrics

MetricTargetAlert Threshold
Charger utilization rate60-80% during peakLess than 30% or over 95%
Reservation fill rateOver 85%Less than 70%
No-show rateLess than 5%Over 10%
Average session revenue$8-15Significant deviation
Charger uptimeOver 97%Less than 95%
ANPR recognition rateOver 97%Less than 90%
Payment success rateOver 99.5%Less than 98%
API P99 latencyUnder 200msOver 500ms
OCPP message deliveryOver 99.9%Less than 99%

Security Architecture

C#
public class SecurityConfig
{
    public AuthConfig Auth { get; set; } = new()
    {
        Provider = "OAuth2",
        TokenExpiry = TimeSpan.FromHours(1),
        RefreshTokenExpiry = TimeSpan.FromDays(30),
        RequireMfa = true,
        AllowedScopes = new[] {
            "reservations:read", "reservations:write",
            "charging:read", "charging:manage",
            "billing:read", "admin:locations"
        }
    };

    public OcppSecurityConfig OcppSecurity { get; set; } = new()
    {
        Protocol = "ocpp2.0.1",
        Transport = "wss",
        AuthMethod = "client_certificate",
        CertificateRotationDays = 90,
        RequireMutualTls = true
    };

    public EncryptionConfig Encryption { get; set; } = new()
    {
        AtRest = "AES-256-GCM",
        InTransit = "TLS_1.3",
        KeyRotationDays = 30,
        PiiFields = new[] {
            "license_plate", "payment_method",
            "home_address", "email"
        }
    };
}

Compliance Requirements

RegulationScopeRequirements
PCI DSS Level 1Payment processingTokenized card storage, annual audit, encryption
GDPREU user dataData portability, right to erasure, consent
CCPACalifornia user dataOpt-out, access requests, deletion
Local parking regulationsParking operationsMax rates, signage, accessible parking %
NEVI StandardsUS federal chargingUptime requirements, payment methods, transparency
OCPP ComplianceCharger interoperabilityStandards-based communication, roaming
OCPP Access Control: OCPP commands like RemoteStartTransaction and UnlockConnector have real-world physical effects. Unauthorized access could cause safety hazards or financial harm. All OCPP commands must be authenticated, authorized, and logged. The system implements a command whitelist per operator, and all commands are logged with operator, user, and timestamp for audit.

23. API Design

The system exposes multiple APIs: a public API for mobile apps and third-party integrations, an operator API for parking operators, and an internal API for inter-service communication. All APIs use RESTful design with JSON payloads and OAuth 2.0 authentication.

Public API (Mobile App)

HTTP
GET    /api/v1/chargers/search?lat={lat}&lng={lng}&radius={km}&connector={type}
GET    /api/v1/locations/{id}
GET    /api/v1/locations/{id}/availability
POST   /api/v1/reservations
GET    /api/v1/reservations/{id}
PUT    /api/v1/reservations/{id}/extend
DELETE /api/v1/reservations/{id}
POST   /api/v1/charging/start
GET    /api/v1/charging/{sessionId}/status
POST   /api/v1/charging/{sessionId}/stop
GET    /api/v1/charging/{sessionId}/invoice
POST   /api/v1/waitlist/join
GET    /api/v1/waitlist/position
GET    /api/v1/route/plan
GET    /api/v1/user/reservations
GET    /api/v1/user/charging-history

Operator API

HTTP
GET    /api/v1/ops/locations
PUT    /api/v1/ops/locations/{id}
GET    /api/v1/ops/locations/{id}/chargers
PUT    /api/v1/ops/chargers/{id}
POST   /api/v1/ops/chargers/{id}/reset
POST   /api/v1/ops/chargers/{id}/firmware
GET    /api/v1/ops/locations/{id}/analytics
PUT    /api/v1/ops/pricing/{locationId}
POST   /api/v1/ops/locations/{id}/demand-response
GET    /api/v1/ops/enforcement/flags
POST   /api/v1/ops/spots/{id}/override

Example: Charger Search Response

JSON
{
    "results": [
        {
            "location_id": "550e8400-e29b-41d4-a716-446655440000",
            "name": "Downtown Parking Garage",
            "address": "123 Main St, San Francisco, CA 94105",
            "distance_km": 0.8,
            "drive_time_minutes": 4,
            "available_spots": 12,
            "total_ev_spots": 24,
            "chargers": [
                {
                    "charger_id": "charger-001",
                    "connector_type": "CCS",
                    "power_kw": 150,
                    "status": "available",
                    "current_price_per_kwh": 0.42,
                    "estimated_15min_charge_kwh": 37.5
                }
            ],
            "amenities": ["restrooms", "convenience_store", "wifi"],
            "rating": 4.6,
            "pricing": {
                "energy_rate": 0.42,
                "parking_rate_per_min": 0.08,
                "reservation_fee": 1.50,
                "surge_active": false
            }
        }
    ],
    "total_results": 8,
    "search_time_ms": 45
}
API Rate Limits: Public API rate limits are 100 requests/minute per user for search, 30 requests/minute for reservation operations, and unlimited for WebSocket connections. Operator API limits are 1000 requests/minute per operator. Rate limit headers are included in every response.

24. Cost Estimation

ComponentMonthly CostNotes
API Gateway + Load Balancer$800AWS ALB + API Gateway
Core Services (10 containers)$3,000ECS Fargate or Kubernetes
OCPP Gateway (WebSocket servers)$1,50075K concurrent connections
PostgreSQL + PostGIS$2,500RDS Multi-AZ with replicas
Redis Cluster$1,200Real-time availability + locks
TimescaleDB (telemetry)$2,00090-day telemetry retention
Kafka$1,500Event streaming
S3 (logs + archives)$300Long-term storage
ML Model Hosting (ANPR)$800GPU instances for plate recognition
Monitoring (Prometheus + Grafana)$500Metrics + alerting
Payment Gateway Fees$2,000~2.9% + $0.30 per transaction
Mapping / Navigation API$400Google Maps / Mapbox
Total~$16,500

Revenue Model

Revenue StreamPer-Unit RateMonthly VolumeMonthly Revenue
Energy sales$0.35/kWh (avg margin $0.10)500,000 kWh$50,000
Parking fees$6/hr (avg margin $2.00)200,000 hours$400,000
Reservation fees$2.00/reservation200,000 reservations$400,000
Fleet subscriptions$500/vehicle/month500 fleet vehicles$250,000
Demand response revenue$100/kW-year1 MW enrolled$8,333
Idle fees$0.25/min (avg $5/session)10,000 sessions$50,000
Total Monthly Revenue~$1,158,333
Unit Economics: At scale (5,000 locations, 75,000 chargers), the system generates approximately $1.16M/month in revenue against $16.5K/month in infrastructure costs — an infrastructure cost of roughly 1.4% of revenue. The marginal cost of adding a charger to an existing location is essentially zero from a systems perspective.

25. Testing Strategy and Interview Q&A

Testing Strategy

The system spans physical hardware, external protocols, financial transactions, and real-time communication. Testing must cover all these domains with appropriate strategies.

Test Levels

LevelFocusToolsCoverage
Unit TestsPricing, availability, state machinesxUnit, Moq90%+ domain logic
Integration TestsReservation creation, OCPP, paymentsTestcontainers80%+ critical paths
Contract TestsOCPP format, API schemaPact, WireMock100% OCPP messages
End-to-End TestsFull reservation-charge-pay flowSelenium, simulated chargersAll critical journeys
Load TestsConcurrent reservations, OCPP throughputk6, Gatling100K concurrent sessions
Chaos TestsCharger offline, network partitionChaos Monkey, ToxiProxyWeekly in staging
C#
[TestClass]
public class ReservationEngineTests
{
    [TestMethod]
    public async Task Should_Prevent_Double_Booking()
    {
        var spotId = Guid.NewGuid();
        var startTime = DateTime.UtcNow.AddHours(1);
        var endTime = DateTime.UtcNow.AddHours(3);

        var engine = CreateReservationEngine();
        var request1 = CreateRequest(
            spotId, startTime, endTime);
        var request2 = CreateRequest(
            spotId, startTime.AddMinutes(30),
            endTime.AddMinutes(30));

        var result1 =
            await engine.CreateReservationAsync(request1);
        var result2 =
            await engine.CreateReservationAsync(request2);

        Assert.IsTrue(result1.IsSuccess);
        Assert.IsFalse(result2.IsSuccess);
    }

    [TestMethod]
    public async Task Should_Handle_Concurrent_Booking_Race()
    {
        var spotId = Guid.NewGuid();
        var startTime = DateTime.UtcNow.AddHours(1);
        var endTime = startTime.AddHours(2);

        var tasks = Enumerable.Range(0, 100)
            .Select(_ => engine.CreateReservationAsync(
                CreateRequest(spotId, startTime, endTime)))
            .ToList();

        var results = await Task.WhenAll(tasks);
        var successes = results.Count(r => r.IsSuccess);

        Assert.AreEqual(1, successes,
            $"Expected exactly 1 success, got {successes}");
    }
}

Interview Q&A Deep Dive

Q1: How do you prevent double-booking of a parking spot?

Answer: Use a distributed lock on the spot + time range key before checking availability and creating the reservation. The lock is acquired via Redis SET NX EX with a 30-second TTL. Inside the lock, we perform an atomic check-and-create: query for conflicting reservations within the time window, and if none exist, insert the new reservation. PostgreSQL's unique index provides a database-level safety net. The combination of application-level locking and database constraints ensures zero double-bookings even under extreme concurrency.

Q2: How do you handle real-time availability for 75,000 charger connectors?

Answer: A multi-tier approach: (1) OCPP heartbeats flow through Kafka to a stream processor that updates a Redis hash per charger with status and timestamp (TTL = 2x heartbeat interval). (2) Reservation state in PostgreSQL is the source of truth for reserved spots. (3) An availability reconciler merges charger status from Redis with reservation state. (4) Search queries hit the Redis-backed availability cache first (sub-millisecond) and fall back to PostgreSQL if stale. This gives real-time accuracy within 10 seconds while keeping search queries fast.

Q3: How does the dynamic pricing engine work?

Answer: The pricing engine computes a price multiplier as a product of factors: time-of-day (0.7x-1.5x), demand (1.0x-2.0x based on utilization), grid electricity cost (from utility API), and location-specific base rate. Final price = base_rate x all_multipliers. Prices are recomputed every 5 minutes per location and cached in Redis. The 5-minute window smooths oscillation. Prices are guaranteed for 15 minutes after a user views them.

Q4: How do you handle charger failures during an active charging session?

Answer: The OCPP gateway monitors charger heartbeats and status notifications. If a charger goes offline mid-session: (1) The charger continues charging (OCPP requires this). (2) Meter values are buffered locally on the charger. (3) When connectivity resumes, the charger sends the backlog. (4) The session manager reconciles any gaps with interpolated data. (5) Billing is based on the charger's own meter (authoritative). For hardware faults, the session is gracefully terminated, the user is notified, and the charger is marked as faulted.

Q5: How do you handle load balancing when grid capacity is limited?

Answer: The load manager polls grid capacity every 30 seconds and distributes available power across active sessions using a priority engine. Higher-priority vehicles (fleet vehicles, time-critical reservations) get more power. If total demand exceeds capacity, lower-priority vehicles are throttred or suspended. The system also integrates with utility demand response programs via OpenADR to automatically reduce load during grid stress events, earning revenue while maintaining user satisfaction.

Q6: How do you design the system for international support and different connector types?

Answer: The connector compatibility matrix maps vehicle types to their compatible connectors (CCS1, CCS2, CHAdeMO, NACS, Type 2). The charger search API accepts a connector_type filter. The route planner cross-references the vehicle profile against available charger types along the route. The system supports multiple currencies, time zones, and local parking regulations via a location-specific configuration. OCPP 2.0.1 adds ISO 15118 Plug and Charge for automatic vehicle identification and payment across networks.

Pre-Interview Checklist

  • Understand OCPP protocol and charger state machines
  • Know how to design real-time availability with multi-source reconciliation
  • Design a reservation system with strong consistency (no double-booking)
  • Understand dynamic pricing algorithms and demand response
  • Know load balancing strategies for grid capacity constraints
  • Discuss ANPR integration and privacy compliance
  • Understand two-phase payment (authorize/capture) for variable-amount sessions
  • Explain charger compatibility and EV route planning
  • Know how to handle charger faults and session resilience
  • Understand waitlist queue management with priority rules

Key Numbers to Remember

When designing or interviewing about this system, these numbers demonstrate the scale and constraints that drive architectural decisions. Understanding these figures helps justify design choices and shows depth of knowledge about production EV charging systems.

MetricValue
Global EV sales (2030 projected)45 million unit sales annually
Total parking locations (target scale)5,000 across metropolitan areas
Total parking spots1,000,000 (5% with EV chargers)
Total charger connectors75,000 (dual-connector stations common)
Daily reservations200,000 (peak 11 QPS)
Concurrent charging sessions (peak)30,000
OCPP heartbeat rate2,500 heartbeats/second across fleet
Meter value updates (during charging)1,000 updates/second
Real-time availability latency targetUnder 10 seconds
Reservation lock TTL30 seconds (distributed lock)
No-show grace period30 minutes after reservation start
Idle fee start threshold30 minutes after charging completes
ANPR recognition accuracy97-99% in good conditions
Price guarantee window15 minutes after user views price
Load balancing polling interval30 seconds
Safety margin for grid capacity90% of maximum utility connection
Demand response revenue$50-150 per kW-year capacity commitment
Infrastructure cost ratio1.4% of gross revenue at scale
OCPP protocol supportBoth 1.6 (JSON/WebSocket) and 2.0.1
Charger compatibility typesCCS1, CCS2, CHAdeMO, NACS, Type 2
Interview Tip: When discussing this system design in an interview, anchoring your discussion with specific numbers from this table demonstrates production-level understanding. For example, saying "at 75,000 connectors with 30-second heartbeats, we process 2,500 heartbeat messages per second through our OCPP gateway" is far more compelling than "we handle lots of chargers." The interviewer sees that you have thought through the actual engineering challenges at scale, not just the abstract architecture.

Parking and EV Charging Reservation System — Senior+ Guide | Ayodhyya