Functional Requirements:
Non-Functional Requirements:
We will use RESTful principles for client-facing operations.
POST /v1/reservationsuser_id, lot_id, vehicle_type, start_time, end_timereservation_id, spot_id, statusPUT /v1/reservations/{reservation_id}/cancelstatus, refund_amountPOST /v1/gates/check-inlot_id, gate_id, identifier (license plate string or QR token)action (open_barrier, deny), spot_idPOST /v1/gates/check-outlot_id, gate_id, identifieraction (open_barrier), pending_chargesPOST /v1/paymentsreservation_id, amount, payment_method_idtransaction_id, statusThe architecture is broken down into modular, independent services to isolate failures and scale components independently.
SELECT ... FOR UPDATE query to lock the specific row of an available parking spot.RESERVED for the specified time block, and the transaction commits. If the row was already locked by another thread, the system catches the failure and retries fetching a different available spot.available_spots_by_type. If the Redis counter is zero, we reject the request immediately without hitting the database.Plate_XYZ: {reservation_id, spot_id, valid_time}.VehicleEnteredEvent.OCCUPIED.VehicleEnteredEvent is not received before the TTL expires, a worker function executes, changes the reservation status to NO_SHOW, applies the penalty fee via the Payment Service, and releases the spot back into the available pool.VehicleExitedEvent is received before the end_time, the system immediately recalculates the cost, triggers a refund via the Payment Service if applicable, and unlocks the spot for new reservations instantly.Your initial API covered the core actions, but a real-world system needs to handle the pre-booking phase. Users need to check capacity and see prices before committing. Also, booking and paying usually require a "hold" mechanism.
Let's add these missing pieces to the API contract:
GET /v1/lots/{lot_id}/availabilitystart_time, end_time, vehicle_typeavailable_spots_countPOST /v1/quoteslot_id, start_time, end_time, vehicle_typequote_id, price, valid_until (Supports dynamic pricing based on current load).POST /v1/reservations/hold -> Changes spot status to HOLD for 5 minutes.POST /v1/reservations/confirm -> Called after payment succeeds. Changes status to RESERVED. If not called within 5 minutes, the TTL expires and the spot reverts to AVAILABLE.The initial design mentioned SELECT ... FOR UPDATE, which is on the right track, but we need to explain how we prevent a bottleneck where hundreds of users are waiting for the lock on the exact same row.
SKIP LOCKED: * When searching for a spot, the query should be: SELECT id FROM spots WHERE type = 'compact' AND status = 'AVAILABLE' LIMIT 1 FOR UPDATE SKIP LOCKED;version column to the spots table.UPDATE spots SET status = 'RESERVED', version = version + 1 WHERE id = 123 AND version = {expected_version};Idempotency-Key in the header for POST requests. If the same key is seen within a short window, the system returns the cached successful response rather than booking a second spot.A parking lot system interacts with the physical world. If AWS goes down, you cannot trap people inside a physical garage. Here is how we architect for resilience:
VehicleEntered and VehicleExited events locally. Once the connection is restored, it syncs those events back to the cloud via Kafka (Eventual Consistency).VehicleEnteredEvent due to a bug, it shouldn't just disappear. It gets pushed to a DLQ so engineers can inspect it and replay it later.