How to Design a Parking & EV Charging Reservation System
Building a Production-Grade Platform — Real-Time Availability, OCPP Chargers, Dynamic Pricing & Energy Management
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.
Real-World Case Studies
| Company | System | Scale | Key Innovation |
|---|---|---|---|
| ChargePoint | Charging network platform | 70,000+ charging ports | Open protocol support, fleet management dashboard |
| Tesla Supercharger | Proprietary charging network | 50,000+ connectors | Vehicle-integrated reservation, battery preconditioning |
| SpotHero | Parking reservation marketplace | 30,000+ parking locations | Dynamic pricing engine, real-time inventory sync |
| AParkMe / Pod Point | EU charging + parking | 6,000+ charge points | Roaming agreements, multi-OCPP backend support |
| Recharge (Evercharge) | Multi-family EV charging | 1,000+ properties | Load 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Wayfinding and Guidance: In-app navigation from the street to the exact parking spot and charger, including floor, row, and spot number.
- License Plate Recognition: ANPR/LPR cameras automatically identify vehicles entering and exiting, enabling frictionless access and accurate parking duration billing.
- Fleet Management: Fleet operators can manage multiple vehicles, set charging preferences, view aggregated billing, and enforce policies.
- 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
| Requirement | Target | Rationale |
|---|---|---|
| Availability | 99.99% | Charging infrastructure is essential — downtime strands drivers |
| Real-Time Accuracy | < 10 seconds latency | Drivers rely on accurate status to make reservation decisions |
| Reservation Consistency | Strong consistency (no double-booking) | Two drivers cannot reserve the same spot simultaneously |
| Charging Throughput | 100K concurrent sessions | Large metropolitan area with thousands of chargers |
| API Latency (P99) | < 200ms | Mobile app responsiveness for real-time search and booking |
| OCPP Latency | < 5 seconds end-to-end | Start/stop commands must reach chargers promptly |
| Data Retention | 7 years billing, 90 days telemetry | Regulatory compliance for financial records |
| Geospatial Query | < 100ms for 50km radius | Location-based search must be fast |
| PCI Compliance | PCI DSS Level 1 | Handling 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
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
| Data | Storage | Rationale |
|---|---|---|
| Parking locations and spots | PostgreSQL + PostGIS | Geospatial queries for location search |
| Real-time spot availability | Redis (hot) + PostgreSQL (durable) | Sub-second reads for availability checks |
| Reservations | PostgreSQL (primary) + Redis (lock) | ACID for booking consistency |
| Charger status (OCPP) | Redis + TimescaleDB | Fast status reads, time-series for telemetry |
| Charging telemetry | TimescaleDB (90 days) then S3 Parquet | Time-series optimized, archival for analytics |
| Payment records | PostgreSQL (encrypted columns) | PCI compliance, audit trail |
| Geospatial index | PostGIS / Redis GEO | Radius-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.
Request Flow: Reserve and Charge
- 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.
- 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.
- 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.
- 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.
- 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.
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
| Source | Latency | Accuracy | Coverage |
|---|---|---|---|
| Parking sensors (ground/ultrasonic) | 1-3 seconds | 99.5% | Only equipped spots |
| OCPP charger status | 5-30 seconds | 99.9% | Charger connectors only |
| ANPR camera detection | 3-10 seconds | 97% | Entrance/exit points |
| Reservation system state | Real-time | 100% (for reserved spots) | Reserved spots only |
| Manual operator override | Immediate | 100% | 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");
}
}
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
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.
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 Message | Direction | Purpose | Frequency |
|---|---|---|---|
| Heartbeat | Charge Point to CMS | Keep-alive, indicates charger is online | Every 30s |
| StatusNotification | Charge Point to CMS | Charger status changed | On status change |
| BootNotification | Charge Point to CMS | Charger startup, registration | On boot |
| StartTransaction | Charge Point to CMS | User initiated charging session | On plug-in |
| StopTransaction | Charge Point to CMS | Charging session ended | On unplug |
| MeterValues | Charge Point to CMS | Energy meter readings | Every 30s during charging |
| RemoteStartTransaction | CMS to Charge Point | Remote start charging (app-initiated) | On user request |
| RemoteStopTransaction | CMS to Charge Point | Remote stop charging | On user request |
| UnlockConnector | CMS to Charge Point | Unlock stuck cable | On user request |
| Reset | CMS to Charge Point | Reboot charger | Maintenance |
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);
}
}
}
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
State Definitions and Behaviors
| State | Reservable? | Chargable? | User Sees |
|---|---|---|---|
| Available | Yes | Yes | Green icon, Ready to charge |
| Reserved | No | Only reservation holder | Yellow icon, Reserved |
| Charging | No | No | Blue icon, In use |
| Suspended | No | Resume only | Orange icon, Paused |
| Faulted | No | No | Red icon, Out of service |
| Offline | No | No | Gray icon, Unavailable |
| Unavailable | No | No | Gray 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.
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
| Factor | Impact on Price | Data Source |
|---|---|---|
| Time of day | Peak hours +30-50% | Historical demand patterns |
| Grid electricity cost | Wholesale rates vary 5x daily | Utility API / wholesale market |
| Location demand | Over 80% utilization = surge | Real-time charger availability |
| Charger power level | DC Fast = premium over Level 2 | Charger configuration |
| Day of week | Weekends may be +20% in business districts | Historical patterns |
| Weather | Extreme cold/heat = +10% | Weather API |
| Special events | Sports/concerts = custom surge | Event calendar integration |
| Membership tier | Premium 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
};
}
}
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
| Event | Trigger | Actions |
|---|---|---|
| ReservationCreated | Booking confirmed | Reserve spot, authorize payment, send confirmation |
| ReservationRemind | 1 hour before start | Push notification with directions and QR code |
| CheckInDetected | ANPR detects vehicle | Open gate, transition to CheckedIn |
| GracePeriodExpired | 30 min after start time | If no check-in: NoShow, release spot |
| ChargingStarted | OCPP StartTransaction | Transition to ActiveCharging |
| ChargingStopped | OCPP StopTransaction | Transition to Finishing, calculate bill |
| DepartureDetected | ANPR detects exit | Transition to Completed, process payment |
| ExtensionRequested | User requests more time | Check 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
| Component | Unit | Example Rate | Notes |
|---|---|---|---|
| Energy consumption | per kWh | $0.35/kWh | Based on metered energy from OCPP |
| Parking time | per minute | $0.10/min ($6/hr) | From check-in to check-out |
| Reservation fee | flat | $2.00 | Charged at booking |
| Idle fee | per min after charge | $0.25/min | Incentivizes prompt removal |
| Time-of-use premium | multiplier | 1.0x - 2.0x | Applied during peak hours |
| Processing fee | per transaction | $0.30 | Payment processing |
Payment Flow
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;
}
}
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."
});
}
}
}
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
| Priority | Condition | Behavior |
|---|---|---|
| 1 (Highest) | Active reservation, on-time | Immediate assignment when spot opens |
| 2 | Waitlist, compatible vehicle | Notified when compatible charger opens |
| 3 | Waitlist, any connector type | Notified when any charger opens |
| 4 (Lowest) | No vehicle detected | Low 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"
};
}
}
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.
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
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.
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
| Violation | Detection | Response |
|---|---|---|
| Wrong vehicle in reserved spot | ANPR mismatch | Alert user, notify enforcement |
| Non-EV in EV charging spot | ANPR + vehicle database | Fine + vehicle must move |
| Overstaying reservation | Sensor + reservation end time | Idle fee charged, spot released |
| ICEing (blocking charger) | Sensor + manual report | Fine + towing after warning |
| Charging complete not moved | OCPP meter + idle threshold | Idle 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.
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 Type | Native Connector | Compatible With | Max Rate |
|---|---|---|---|
| Tesla Model 3/Y/S/X | NACS (Tesla) | NACS, CCS (adapter) | 250 kW |
| CHEVROLET Bolt / Ford Mach-E | CCS1 | CCS1, NACS (2025+) | 150 kW |
| Nissan Leaf | CHAdeMO | CHAdeMO, CCS (adapter) | 50-100 kW |
| BMW iX / Mercedes EQS | CCS2 (EU) | CCS2, Type 2 (AC) | 150-200 kW |
| Rivian R1T/R1S | CCS1 | CCS1, 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;
}
}
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
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
| Strategy | How It Works | Best For |
|---|---|---|
| Static Load Balancing | Fixed maximum power per charger | Small locations |
| Dynamic Load Balancing | Power distributed by demand/priority | Medium locations |
| Smart Charging (OCPP Profile) | CMS sends charging profiles via OCPP | Large locations |
| V2G (Vehicle-to-Grid) | Vehicles discharge back to grid | Future-ready locations |
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.
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
| Scenario | Rule | Fee |
|---|---|---|
| Extend before arrival | Available if spot free after end time | Reservation fee for new period |
| Extend during charging | Available if spot not reserved | Energy + parking for new period |
| Extend after reservation end | 15 min grace, then idle fees | Idle fee + reservation fee |
| Auto-extension (charging) | If charging over 80% at end, auto-extend 30 min | Normal 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");
}
}
}
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
| Metric | Target | Alert Threshold |
|---|---|---|
| Charger utilization rate | 60-80% during peak | Less than 30% or over 95% |
| Reservation fill rate | Over 85% | Less than 70% |
| No-show rate | Less than 5% | Over 10% |
| Average session revenue | $8-15 | Significant deviation |
| Charger uptime | Over 97% | Less than 95% |
| ANPR recognition rate | Over 97% | Less than 90% |
| Payment success rate | Over 99.5% | Less than 98% |
| API P99 latency | Under 200ms | Over 500ms |
| OCPP message delivery | Over 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
| Regulation | Scope | Requirements |
|---|---|---|
| PCI DSS Level 1 | Payment processing | Tokenized card storage, annual audit, encryption |
| GDPR | EU user data | Data portability, right to erasure, consent |
| CCPA | California user data | Opt-out, access requests, deletion |
| Local parking regulations | Parking operations | Max rates, signage, accessible parking % |
| NEVI Standards | US federal charging | Uptime requirements, payment methods, transparency |
| OCPP Compliance | Charger interoperability | Standards-based communication, roaming |
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
}
24. Cost Estimation
| Component | Monthly Cost | Notes |
|---|---|---|
| API Gateway + Load Balancer | $800 | AWS ALB + API Gateway |
| Core Services (10 containers) | $3,000 | ECS Fargate or Kubernetes |
| OCPP Gateway (WebSocket servers) | $1,500 | 75K concurrent connections |
| PostgreSQL + PostGIS | $2,500 | RDS Multi-AZ with replicas |
| Redis Cluster | $1,200 | Real-time availability + locks |
| TimescaleDB (telemetry) | $2,000 | 90-day telemetry retention |
| Kafka | $1,500 | Event streaming |
| S3 (logs + archives) | $300 | Long-term storage |
| ML Model Hosting (ANPR) | $800 | GPU instances for plate recognition |
| Monitoring (Prometheus + Grafana) | $500 | Metrics + alerting |
| Payment Gateway Fees | $2,000 | ~2.9% + $0.30 per transaction |
| Mapping / Navigation API | $400 | Google Maps / Mapbox |
| Total | ~$16,500 |
Revenue Model
| Revenue Stream | Per-Unit Rate | Monthly Volume | Monthly 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/reservation | 200,000 reservations | $400,000 |
| Fleet subscriptions | $500/vehicle/month | 500 fleet vehicles | $250,000 |
| Demand response revenue | $100/kW-year | 1 MW enrolled | $8,333 |
| Idle fees | $0.25/min (avg $5/session) | 10,000 sessions | $50,000 |
| Total Monthly Revenue | ~$1,158,333 |
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
| Level | Focus | Tools | Coverage |
|---|---|---|---|
| Unit Tests | Pricing, availability, state machines | xUnit, Moq | 90%+ domain logic |
| Integration Tests | Reservation creation, OCPP, payments | Testcontainers | 80%+ critical paths |
| Contract Tests | OCPP format, API schema | Pact, WireMock | 100% OCPP messages |
| End-to-End Tests | Full reservation-charge-pay flow | Selenium, simulated chargers | All critical journeys |
| Load Tests | Concurrent reservations, OCPP throughput | k6, Gatling | 100K concurrent sessions |
| Chaos Tests | Charger offline, network partition | Chaos Monkey, ToxiProxy | Weekly 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.
| Metric | Value |
|---|---|
| Global EV sales (2030 projected) | 45 million unit sales annually |
| Total parking locations (target scale) | 5,000 across metropolitan areas |
| Total parking spots | 1,000,000 (5% with EV chargers) |
| Total charger connectors | 75,000 (dual-connector stations common) |
| Daily reservations | 200,000 (peak 11 QPS) |
| Concurrent charging sessions (peak) | 30,000 |
| OCPP heartbeat rate | 2,500 heartbeats/second across fleet |
| Meter value updates (during charging) | 1,000 updates/second |
| Real-time availability latency target | Under 10 seconds |
| Reservation lock TTL | 30 seconds (distributed lock) |
| No-show grace period | 30 minutes after reservation start |
| Idle fee start threshold | 30 minutes after charging completes |
| ANPR recognition accuracy | 97-99% in good conditions |
| Price guarantee window | 15 minutes after user views price |
| Load balancing polling interval | 30 seconds |
| Safety margin for grid capacity | 90% of maximum utility connection |
| Demand response revenue | $50-150 per kW-year capacity commitment |
| Infrastructure cost ratio | 1.4% of gross revenue at scale |
| OCPP protocol support | Both 1.6 (JSON/WebSocket) and 2.0.1 |
| Charger compatibility types | CCS1, CCS2, CHAdeMO, NACS, Type 2 |