System requirements
Functional:
Here is the list of the most essential operations we should support for the parking system:
- the check for available places, it will be displayed on the entry SPOTS: 10
- the "enter" button that prints a ticket and opens the gate, and tracks that a vehicle entered, and closes the gate and decrements the free spots count
- the payment system that expects the ticket and, once the payment succeeds, marks the ticket as payed
- the exit ticket check that opens the gate in case the ticket is payed and does not violate some time period to exit, and increments the free slots
For now, we ignore
- types of vehicles as well as
- the reservation flow,
- the spot allocation, and
- fallbacks, e.g., in case the ticket is lost
Non-Functional:
Here is the list we prioritize:
- secure payments
- reliability, the system should be available to support customers to use the parking even when some components go down with the downtime up to a few minutes
- consistency, the system should never lead to double counting or double or not counted payments
Here is not essential list:
- scalability is limited because we'll never have millions of vehicles
- performance is not in the middle because even a few second delays in most of the operations are acceptable
Security
Capacity estimation
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.
API design
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).
Database design
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.
High-level design
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.
Request flows
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:
- ISSUED - default on creation
- ISSUED -> ACTIVATED - on actually entering the parking, tracked by a sensor
- ACTIVATED -> PAYMENT_PENDING - on initiating the payment
- ACTIVATED -> error - when the ticket is never payed, this should be reported and investigated by the stuff
- PAYMENT_PENDING -> PAYED - when the payment succeeded
- PAYMENT_PENDING -> PAYMENT_FAILED - on the failure, may retry
- PAYED -> ACTIVATED -> when too long took place between the payment and the exit
- PAYED -> ERROR - when the ticket was paid yet the car never exited, should be handled manually
- PAYED -> EXITED - the end of the happy path
Detailed component design
I already provided detailed DB schemas above.
Trade offs/Tech choices
Relational because ACID. REST for simplicity. No need for GQL since the devices are simple and models are simple.
Failure scenarios/bottlenecks
The RW instance of the database may go down. So we make the RO as the primary.
Future improvements
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.