## 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.