## 1. Requirements
### 1.1 Functional Requirements (FR)
#### Core Operations
* **Vehicle Entry**:
* Detect vehicle at entry gate and identify type (CAR, MOTORBIKE, TRUCK, EV, etc.).
* Assign a suitable parking spot or deny entry if the lot is full.
* Issue a ticket (QR, barcode, or license-plate-linked) with an entry timestamp.
* **Vehicle Exit**:
* Scan ticket/license plate at the exit gate.
* Compute fees based on duration and pricing rules.
* Accept multiple payment methods and open the barrier upon success.
* Close the ticket and free the assigned spot.
* **Spot Allocation**:
* Support multiple spot types: `COMPACT`, `LARGE`, `MOTORBIKE`, `EV_CHARGING`, `DISABLED`.
* Configurable allocation rules: Closest-to-entry, floor preference, EV-only, etc.
* **Real-time Availability**:
* Query available spots by lot, floor, or spot-type.
* Provide real-time counts and zone-based guidance.
* **Payments**:
* Support cash (optional), card, and e-wallets.
* Handle refund/cancel policies and grace periods.
* Support lost-ticket fee policies.
#### Management & Audit
* **Admin Operations**:
* Manage parking lot structure (lots, floors, spots).
* Configure pricing rules and seasonal promotions.
* Manual overrides: Force open/close gates, mark spots for maintenance.
* **Enforcement & Audit**:
* Maintain a complete history of tickets, payments, and spot changes.
* Manual dispute handling and overrides.
#### No-Show & Hold Policies
* **Reservation Hold TTL**: Each reservation creates a time-limited hold (e.g., 15 minutes). If the vehicle does not arrive within the TTL, the hold expires automatically and the spot is released back to the available pool.
* **No-Show Handling**: After hold expiry, the system marks the reservation as `NO_SHOW`, triggers a configurable penalty (e.g., partial charge, strike counter), and publishes a `HoldExpired` event so downstream services (analytics, billing) can react.
* **Grace Period**: A short buffer (e.g., 5 minutes) after TTL expiry before the spot is actually released, to account for minor delays.
#### Nice-to-Have
* **License Plate Recognition (LPR)**: Automated entry/exit without physical tickets.
* **Reservations**: Pre-booking for premium members or special events.
* **Multi-lot Management**: Portfolio-wide management from a single interface.
### 1.2 Non-Functional Requirements (NFR)
* **Availability**: Gates must remain operational even if cloud connectivity is lost (99.9%+ target).
* **Latency**: Fast throughput at gates; ticket issuance and allocation should be `< 300ms p95`.
* **Consistency**: Strong consistency for spot occupancy to prevent double-allocation.
* **Scalability**: Handle peak bursts (e.g., stadium events) with high contention on allocation.
* **Reliability**: Offline-first edge support with local cache synchronization.
* **Security**: Role-Based Access Control (RBAC), audit logs, and PCI compliance (tokenization).
* **Data Privacy**: Encryption of PII (license plates) at rest and clear retention policies.
## 2. API Design
### 2.1 Key Domain Objects
```plantuml
@startuml
class ParkingLot {
lotId: string
name: string
address: string
timezone: string
}
class Floor {
floorId: string
lotId: string
level: integer
}
class Spot {
spotId: string
floorId: string
type: COMPACT | LARGE | MOTORBIKE | EV_CHARGING | DISABLED
status: AVAILABLE | OCCUPIED | OUT_OF_SERVICE | RESERVED
zone: string
}
class Vehicle {
plate: string?
type: string
}
class Ticket {
ticketId: string
lotId: string
entryGateId: string
entryAt: ISO8601
assignedSpotId: string
status: ACTIVE | CLOSED | LOST
}
class Payment {
paymentId: string
ticketId: string
amount: decimal
currency: string
method: CARD | CASH | WALLET
status: string
providerRef: string
}
ParkingLot "1" *-- "many" Floor
Floor "1" *-- "many" Spot
Ticket "1" -- "1" Vehicle : vehicle
Ticket "1" -- "1" Spot : assignedSpotId
Ticket "1" -- "many" Payment : ticketId
@enduml
```
### 2.2 Public-facing APIs
#### Entry Flow
`POST /v1/tickets/entry`
```json
// Request
{
"lotId": "L1",
"gateId": "GATE_E_01",
"vehicle": { "type": "CAR", "plate": "SGP1234A" },
"preferences": { "needEvCharging": false, "needDisabled": false }
}
// Response
{
"ticketId": "T_9f...",
"issuedAt": "2026-02-24T10:01:22+08:00",
"spot": { "spotId": "S_2_041", "floorId": "F2", "zone": "B" },
"barcode": "....",
"status": "ACTIVE"
}
```
#### Exit Quote
`GET /v1/tickets/{ticketId}/quote`
```json
{
"ticketId": "T_9f...",
"entryAt": "...",
"exitAt": "...",
"amount": 6.50,
"currency": "SGD",
"breakdown": [{"rule": "weekday_hourly", "amount": 6.50}]
}
```
#### Payment
`POST /v1/payments`
```json
{
"ticketId": "T_9f...",
"method": "CARD",
"paymentToken": "tok_xxx",
"idempotencyKey": "uuid-unique"
}
```
#### Exit Confirmation
`POST /v1/tickets/{ticketId}/exit`
```json
{
"gateId": "GATE_X_02",
"paidPaymentId": "P_12...",
"vehiclePlate": "SGP1234A"
}
```
#### Availability
`GET /v1/lots/{lotId}/availability?spotType=COMPACT`
```json
{ "lotId": "L1", "counts": { "COMPACT": 120, "EV_CHARGING": 8 } }
```
### 2.3 Admin APIs
* `PUT /v1/spots/{spotId}`: Update status or type.
* `POST /v1/pricingRules/publish`: Publish a new rule version.
* `GET /v1/reports/occupancy?from=...&to=...`: Generate occupancy reports.
### 2.4 API Correctness Rules
1. **Idempotency**: All state-changing endpoints must support `Idempotency-Key` to safely handle retries.
2. **Concurrency**: Gate endpoints must tolerate retries without double-allocating spots.
3. **Clock Source**: Use server time (anchored to the lot's timezone) for all billing calculations.
## 3. High-Level Design
### 3.1 Architecture Style (Edge + Central)
The system follows a classic hybrid architecture:
* **Edge (On-prem)**: Local gate controllers for low-latency hardware integration and offline fallback.
* **Central (Cloud)**: Portfolio management, global pricing rules, reporting, and payment orchestration.
### 3.2 Logical Services
* **Gate Service (Edge)**: Hardware interfacing (barriers, scanners, LPR) with local fallback logic.
* **Ticket Service**: Manages ticket lifecycle (Active -> Closed/Lost).
* **Spot Inventory Service**: Source of truth for spot states and real-time counts.
* **Allocation Service**: Policy-based engine for choosing the best available spot.
* **Pricing Service**: Versioned engine to compute fees based on duration and lot rules.
* **Payment Service**: Integrates with Payment Service Providers (PSP) and manages transaction states.
* **Event Bus**: Kafka/Pulsar stream for propagating state changes (TicketCreated, SpotOccupied) to analytics and monitoring.
### 3.3 Data Stores
* **Spot Inventory DB**: Requires strong consistency (Postgres with row-level locks or Redis + Lua).
* **Ticket DB**: Transactional store, partitioned by `lotId` or time for performance.
* **Event Store**: Distributed log (Kafka) for durable event streaming.
## 4. Detailed Component Design
### 4.1 Component Interaction Overview
```plantuml
@startuml
skinparam componentStyle rectangle
package "Edge" {
[Gate Service] as Gate
}
package "Central" {
[Ticket Service] as Ticket
[Spot Inventory Service] as SpotInv
[Pricing Service] as Pricing
[Payment Service] as Payment
}
[Event Bus] as EventBus
actor "Vehicle" as Vehicle
Vehicle --> Gate : arrives / exits
Gate --> Ticket : CreateTicket / ExitRequest
Ticket --> SpotInv : AllocateSpot
SpotInv --> EventBus : SpotOccupied / SpotReleased
Ticket --> Pricing : RequestQuote
Pricing --> Ticket : Quote
Ticket --> Payment : process payment
EventBus ..> [Analytics\nAlerts\nCache Invalidation] : feeds
@enduml
```
**Flow summary**:
1. **Entry**: Gate Service → Ticket Service creates ticket → Allocation Service (within Spot Inventory) atomically reserves a spot → Event Bus publishes `SpotOccupied` → Gate opens.
2. **Exit**: Gate Service → Ticket Service looks up ticket → Pricing Service computes quote → Payment Service processes payment → Spot Inventory releases spot → Event Bus publishes `SpotReleased` → Gate opens.
3. **Async**: Event Bus feeds analytics, cache invalidation, alerting, and audit log consumers.
### 4.2 Spot Inventory + Allocation
#### Atomic Reservation Algorithm (Optimistic Concurrency)
To prevent overselling when two users try to reserve the same spot simultaneously, the system uses Compare-And-Swap (CAS) via a conditional `UPDATE`:
```sql
SELECT spot_id, version FROM spots
WHERE lot_id = ? AND type = ? AND status = 'AVAILABLE'
ORDER BY distance_to_gate ASC
LIMIT 10
FOR UPDATE SKIP LOCKED;
UPDATE spots
SET status = 'HELD', held_by = ?, held_until = NOW() + INTERVAL '15 minutes', version = version + 1
WHERE spot_id = ? AND status = 'AVAILABLE' AND version = ?;
```
* If `RowsAffected == 0`, another transaction won the race — retry with the next candidate from the batch.
* `FOR UPDATE SKIP LOCKED` ensures concurrent transactions don't block each other; they simply skip already-locked rows and try the next candidate.
* The `version` column provides an additional optimistic concurrency guard against stale reads.
#### Two-Phase Hold → Commit
Allocation is split into two phases to prevent conflicts during the window between spot selection and vehicle arrival:
| Phase | Status | Duration | Description |
|-------|--------|----------|-------------|
| **Hold** | `HELD` | TTL (default 15 min) | Spot is temporarily reserved. Other allocations skip it. |
| **Commit** | `OCCUPIED` | Until exit | Vehicle physically arrives; gate confirms and commits the hold. |
**Re-check at commit time**: When the vehicle arrives and the gate commits the hold, the system re-validates:
```sql
UPDATE spots
SET status = 'OCCUPIED', held_by = NULL, held_until = NULL
WHERE spot_id = ? AND status = 'HELD' AND held_by = ?;
```
If `RowsAffected == 0`, the hold has expired or been revoked — the entry flow restarts with a fresh allocation.
#### Expired Holds Cleanup
A background scheduler reclaims expired holds every 60 seconds:
```sql
UPDATE spots
SET status = 'AVAILABLE', held_by = NULL, held_until = NULL
WHERE status = 'HELD' AND held_until < NOW();
```
Each reclaimed hold publishes a `HoldExpired` event so no-show penalties and analytics can fire.
#### Idempotent Allocation (Duplicate Request Protection)
Each allocation request carries an `idempotencyKey` (generated by the gate). Before creating a new hold, the service checks:
```sql
SELECT spot_id FROM allocations WHERE idempotency_key = ?;
```
If a record exists, the previously allocated spot is returned without creating a duplicate hold.
#### Performance Optimization: Zone-Counters
Maintain a `zone_availability` table with transactional counters:
1. Identify a zone with `available_count > 0`.
2. Query for spots only within that high-probability zone to reduce lock contention across the entire table.
3. Counters are updated transactionally alongside spot status changes (same DB transaction).
#### Handling Overlapping Time Intervals
For reservation-based parking (pre-booked time slots), the system prevents overlapping bookings on the same spot:
```sql
SELECT 1 FROM reservations
WHERE spot_id = ?
AND status IN ('CONFIRMED', 'HELD')
AND start_time < ? -- requested end
AND end_time > ?; -- requested start
```
If a row is returned, the spot is unavailable for the requested interval. The allocation engine skips to the next candidate.
### 4.3 Ticket Service
* **State Machine**: `ACTIVE` → (`CLOSED` | `LOST`). `CLOSED` requires payment satisfaction.
* **No-Show State**: If a hold expires without vehicle arrival, the ticket transitions to `NO_SHOW` and a penalty policy is evaluated.
* **Plate Handling**: License plates are hashed and encrypted. Original values are stored only if required for compliance.
### 4.4 Pricing Service
Uses a versioned JSON rules engine to build a timeline:
* Evaluate time bands (hourly, daily, flat).
* Apply priority rules (holidays, guest types).
* Return a transparent breakdown for the user.
### 4.5 Payment Service
* **States**: `INITIATED` -> `AUTHORIZED` -> `CAPTURED/FAILED` -> `REFUNDED`.
* **PSP Integration**: Uses tokenization to minimize PCI scope on internal systems.
* **Idempotency**: Every payment request requires an `idempotencyKey`. The PSP adapter de-duplicates on this key to prevent double charges on retries.
### 4.6 Edge Gate Service (Offline-First)
#### Local Cache & Staleness Mitigation
* **Local Fallback**: Gate keeps a cache of availability and pricing rules.
* **Cache Invalidation**: The Event Bus pushes `SpotOccupied` / `SpotReleased` events to edge subscribers. On receipt, the local cache is updated within seconds. A full cache refresh runs every 5 minutes as a safety net.
* **Stale Cache Guard**: When online, every allocation request goes to the central Spot Inventory Service (source of truth). The local cache is only used for display/guidance. Actual booking always hits the central DB with the atomic CAS query, so stale cache **cannot** cause incorrect bookings — it can only show temporarily wrong availability counts.
* **Conflict Mitigation**: If offline, issue tickets with a local sequence prefix and sync upon restoration. Operates on a "pre-allocated quota" (a fixed number of spots reserved for offline use) to prevent overselling while disconnected.
### 4.7 Observability
* **Allocation Accuracy**: Cross-check sensor data vs database states.
* **Latency Metrics**: Track p95 for entry/exit gates.
* **Operational Health**: Monitor offline mode durations and synchronization success rates.
* **Hold Metrics**: Track hold-to-commit conversion rate, expired hold count, and no-show rate.
---
## 5. Scalability & High-Traffic Design
### 5.1 Load Balancing
* An **Application Load Balancer (ALB)** fronts the Ticket Service and Spot Inventory Service, distributing requests across multiple instances.
* Gate-to-cloud traffic is routed through a regional load balancer with health checks and automatic failover.
### 5.2 Horizontal Scaling
* **Stateless Services**: Ticket, Pricing, Payment, and Allocation services are stateless and scale horizontally behind the load balancer.
* **Database Scaling**: Spot Inventory DB is partitioned by `lotId`. Each lot's data is independent, so high contention on one lot does not affect others.
* **Read Replicas**: Availability queries (`GET /availability`) hit read replicas to offload the primary.
### 5.3 Handling Demand Spikes (Stadium Events)
* **Pre-allocated Batches**: For known events, the system pre-segments available spots into allocation batches. Each service instance is assigned a batch, eliminating cross-instance contention on the same rows.
* **Queue-based Throttling**: During extreme surges, entry requests are funneled into a message queue (SQS/Kafka). Workers consume at a controlled rate, preventing database overload. Vehicles see a "processing" status with estimated wait time.
* **Auto-scaling**: Cloud infrastructure auto-scales service instances based on queue depth and request latency metrics.
### 5.4 Distributed Architecture
```plantuml
@startuml
node "Global LB" as GLB
node "Region A" {
component "Services" as SvcA
database "DB (lotA)" as DBA
}
node "Region B" {
component "Services" as SvcB
database "DB (lotB)" as DBB
}
node "Region C" {
component "Services" as SvcC
database "DB (lotC)" as DBC
}
queue "Event Bus\n(Kafka/SNS)" as EventBus
component "Analytics" as Analytics
component "Audit Log" as AuditLog
GLB --> SvcA
GLB --> SvcB
GLB --> SvcC
SvcA --> DBA
SvcB --> DBB
SvcC --> DBC
SvcA --> EventBus
SvcB --> EventBus
SvcC --> EventBus
EventBus --> Analytics
EventBus --> AuditLog
@enduml
```
* **Data Locality**: Each parking lot's data lives in the region closest to the physical lot. No cross-region queries for real-time operations.
* **Event Bus**: Cross-region events flow through the Event Bus for portfolio-wide analytics and reporting.
* **No Global Locks**: Because spot allocation is lot-scoped, there is no need for distributed locks across regions.