GET /spots/available
?lot_id, vehicle_type, start_time, end_time — returns available spots with pricing
GET /spots/{spot_id}
Spot details, current status (available/reserved/occupied)
POST /reservations
Body: {spot_id, user_id, vehicle_id, start_time, end_time} — creates reservation, locks spot
GET /reservations/{reservation_id}
Fetch reservation details and current state
PUT /reservations/{reservation_id}/cancel
Cancel before start_time; triggers refund eligibility check
POST /payments
Body: {reservation_id, payment_method_token} — charges user, returns payment_id
POST /payments/{payment_id}/refund
Body: {reason, amount} — partial/full refund on early exit or no-show
POST /gate/check-in
Body: {reservation_id | license_plate | qr_code, gate_id} — validates entry, opens gate
POST /gate/check-out
Body: {reservation_id | license_plate, gate_id} — logs exit time, triggers billing reconciliation
VEHICLES & USERS
POST /users/{user_id}/vehicles
Register vehicle (plate, type: compact/SUV/motorcycle/EV)
GET /users/{user_id}/reservations
Reservation history with statuses
The system is built as a set of loosely coupled microservices sitting behind an API Gateway that handles authentication, rate limiting, and routing. The core services — Reservation, Payment, Spot Management, Gate, and Notification — each own their own database, communicating asynchronously via a Kafka event bus for non-critical flows and synchronously via direct HTTP/gRPC calls where latency matters (gate validation, payment at checkout). Gate hardware runs a lightweight edge agent with a local cache so physical barriers never depend on backend availability. A Redis layer sits in front of spot availability to serve high-read-volume queries without hitting the database.
Spot Management Service maintains real-time availability in Redis as a sorted set keyed by lot:floor:spot_id with values encoding the occupied time ranges. This allows O(log n) range queries to find free windows. The canonical source of truth is PostgreSQL; Redis is a write-through cache.
No-show handler is a scheduled job (cron every 5 minutes) that queries all reservations with status=CONFIRMED and start_time + grace_period < NOW(). For each match it fires a no_show.triggered Kafka event, which the Reservation Service consumes to update the status and the Payment Service consumes to charge the no-show fee (typically a percentage of the booking). The spot is released back into availability atomically.
Early departure — on gate/check-out, the Gate Service logs the actual exit timestamp. It then computes the unused duration and emits a checkout.early event. The Payment Service calculates the refund amount based on the lot's early-exit policy (e.g. prorated minus a minimum fee) and issues a partial refund via the payment provider's API.
Gate hardware runs a lightweight edge agent that syncs a local SQLite snapshot of all upcoming reservations (next 24h) every few minutes via the Gate Service. This ensures the gate can validate and open in under 200ms even during a network partition. All events are buffered locally and flushed when connectivity resumes.
Vehicle type accommodation — spots are tagged with types: compact, standard, SUV, motorcycle, and EV (which also carry a charger flag). The search API filters availability by vehicle type and the frontend surfaces only compatible spots. EV spots also expose charger status as a live field pulled from the charger management system.
Horizontal scaling
Each microservice is stateless and can be scaled independently behind a load balancer — you add Payment Service instances during peak hours without touching the Gate Service. Kafka partitions are the unit of parallelism on the consumer side; scaling a consumer group is just adding more instances, and Kafka rebalances partition ownership automatically. The main stateful bottlenecks are Redis and PostgreSQL — Redis scales horizontally via clustering and consistent hashing across nodes, while PostgreSQL scales reads through read replicas and handles write contention on hot spots (like a popular lot at peak time) through partition-level distributed locks rather than row-level locks on a single record.