Here is the list of the most essential operations we should support for the parking system:
For now, we ignore
Here is the list we prioritize:
Here is not essential list:
Security
The system is estimated to handle 1000 vehicles yet should be able to scale to 100 000 capacity with a car entering or leaving up to each 10 seconds.
Let's focus on the HTTP backend endpoints to cover the functional requirements in the REST style.
PATCH /v1/slots {"capacity": 100, "used": 0} - for the initial configuration of the system
GET /v1/slots {"capacity": 100, "used": 20} - to display if there are free slots in the parking
POST /v1/tickets {} - on pressing the button to enter the parking to get ticket ID, returns ticket ID UUID
PATCH /v1/tickets/{ticketID} {"state": "ACTIVATED"} - to, once the ticket is effectively used to enter the parking, put the ticket to the ACTIVATED, PAID, or EXITED state, while the default state is ISSUED, signing the driver entered the parking, paid, or exited
GET /v1/tickets to get ticket info like activatedAt or that the payment succeeded
POST /v1/tickets/{ticketID}/payment/initiate and returns the payment secret to initiate the payment flow
POST /v1/tickets/{ticketID}/payment/done for stripe to call as a web hook to indicate that the payment succeeded
This is enough for the current simple flow. The "slots" endpoints inform on available slots. Treat it as a metadata. Whereas "tickets" endpoints are responsible for the lifetime of the ticket, including enter, pay, and exit.
RBAC different for slots config (more restricted), ticket lifetime (typical), and payment completion (only for stripe).
A database such a postgresql is perfect here. We care about ACID. And real-time is not so important. Moreover, we have a limited count of rows.
It's enough to have max_slots as a variable (DECLARE @max_slots AS INT = 100). The PATCH slots will modify it. As well as occupied_slots. No need for tables here.
The main table is "tickets" with ticketID as a primary key. The important field is state with values ISSUED, ACTIVATED, PAYMENT_PENDING, PAYMENT_FAILED, PAID, EXITED, or ERROR. Also we need some metadata fields like "createdat" and "updatedat".
It is super useful to have an auxiliary table "ticketactivity" that saves the timestamp for each ticket action. The ticketID is a primary key. The "performedat" is the important timestamp field with the activity enum such as ENTRY, PAYMENT, EXIT. We'll use it for covering edge cases such as a car entered parking, paid right away, and then stayed for many hours. In this case, the backend will automatically expire the status from PAID to ACTIVATED.
The machines such as slots available machine, the payment machine, and the "enter" button machine will use REST API and have appropriate RBAC access based on API keys. In case a machine is compromised, we may revoke a key. All the requests go through load balancers to API service. All the communication is secured with HTTPs. The API servers are stateless and scale horizontally based on the usage needs. The database has a RW and a RO instance. In case of a disaster the RO instance may become RW, in solutions such as Aurora.
The central piece of the design is the lifetime of a ticket. It's a state machine with a finite set of states and defined transitions between them. The real-like states and transitions will be more complex, yet the core provided below should serve as a good starting point.
States: ISSUED, ACTIVATED, PAYMENT_PENDING, PAYMENT_FAILED, PAYED, EXITED, ERROR
Transitions:
I already provided detailed DB schemas above.
Relational because ACID. REST for simplicity. No need for GQL since the devices are simple and models are simple.
The RW instance of the database may go down. So we make the RO as the primary.
We may consider adding a distributed cache such as redis for very active parkings. The design may touch the other flows such as admin console for the stuff or the booking system.