My Solution for Design an Efficient Parking Lot System with Score: 8/10
by orbit_ethereal636
System requirements
Functional:
- Assumption - The ParkingLot operator uses a centralized backend system to support multiple geographical locations , where the operator is managing the ParkingLot .
- The parking lot supports the following car size categories - Compact (C) , Standard (S) and Large (L) . The Gate Clerk at the entry point manually determine the car type
- There are 3 personas that use this system viz. -
- Admin - admin user is responsible for the CRUD the parking lot structure definition , charging policy etc using the admin apis.
- GateClerk - GateClerk mans the entry exit points , he is responsible for :-
- issuing ticket on entry
- generating invoice on exit
- accepting payment through existing payment gateway integrations or cash transaction.
- generating a payment slip .
- SystemUser - System users are used for automating various non transactional task( invoking specific api s ) in the overall system like -
- Display board of current availability status
- Optional Access Control at the Parking slot such that only allocated Cars are able to park
- There is no separate persona created for User( Car Driver ) at this point . There are no direct interaction of the User with the backend system currently . But in future if a self-serve ( web or mobile based ) interface is desired - 1 option is to use a common "headless User" personal - wherein no User PII is captured or stored.
- *Checkin flow* - There are multiple entry gates to the Parking Lot . When a car approaches , the clerk invokes the the createTicket api to generate a ticket with the car registration number and the car size category and registration number . Ticket is generated if and only if the car can be allocated a slot . Ticket generated is for the particular SlotId .
- Out of scope - pre-booking and scheduling of the parking.
- *Check Out flow* - GateClerk does the following steps -
- generating invoice on exit
- accepting payment through existing payment gateway integrations or cash transaction.
- generating a payment slip .
- *Admin Flow* - Following functionalities are categorized as admin -
- Create/Update ParkingLot - admins can define a new Parking Lot at a given geographical location and define the list of parking slots available under that ParkingLot. The ParkingLot entity is the aggregate root. ParkingLot entity has operation status-ACTIVE | INACTIVE.
- Define Charging Policies and running marketing campaign through coupons etc.
- *Reporting and Reconciliation Flow* - Scheduled daily jobs to run reconciliation flow using the the invoice and the payment receipt data captured . For Payment Gateway integration , a clearing request file has to be generated from these sources and sent to various Payment Gateway . There also needs to be monthly or adhoc report operational and business reports to be generated from the data . There might be need for business and operation dashboards.
- *Display Info flows* - There could be realtime electronic display boards at the ParkingLot location to display the current realtime availability status .
- *Operations dashboard* - There needs to be operations dashboard for a realtime view into the system , that enables possible manual actions for various scenarios like identfying possible abandoned vehicles or helping law enforcement to trace a particular parked vehicle etc
- *Data management* - As this system generates a lot of ephemeral transactional data, there has to be a policy of data archival and retention( if required by law)
Non-Functional:
- Strong consistency - correctness of transaction.
- Low latency of transanctions
- High scalability
- High Availability ( Fault tolerance )
Capacity estimation
Estimate the scale of the system you are going to design...
API design
**Admin api**
- create/Update ParkingLot - POST /parkingLots
- Bulk create ParkingSLots -- POST /parkingLots/<parkingLotCode>/bulkCreateSlots
- Create/Update/Delete single parkingSlots -
POST/PUT/DELETE /parkingLots/
**Transaction api**
- Checkin flow - create ticket -- POST /parkingLots/<parkingLotCode>/tickets
- Checkout flow 1 - create invoice -- POST /parkingLots/<parkingLotCode>/tickets/<ticketId>/invoices
- Checkout flow 2 - record Payment - POST /parkingLots/<parkingLotCode>/tickets/<ticketId>/invoices/<invoiceId>/paymentInfos
- Checkout flow 3 - Complete checkout and return acknowledgement. - Checkout flow 2 - record Payment -- POST /parkingLots/<parkingLotCode>/tickets/<ticketId>/completeCheckout
**Read apis**
- Vacancy Summary ParkingSlot - GET /parkingLots/<lot_id/vacancySummary
Database design
Table ParkingSlot is partitioned by lot_code field to allow for efficient access and avoid full table scans
Table Ticket is partitioned by entry_epoch_time_min ( yyyy-mm component ) to faciliated archival
Table ParkingSlot and Table Ticket has composite unique key defined on fields 'lot_code' , 'vehicle_reg_num' and 'entry_epoch_time_min' ( 'current_entry_epoch_time_min' for Table ParkingSLot )
Table ParkingSlot has composite primary key comprising of columns 'lot_code' and 'slot_code'.
Table ParkingSlot has secondary composite index defined on columns 'lot_code' , 'size_type_enum_C_S_L' and 'is_empty'
Table Ticket has composite primary key comprising of columns 'id' and 'lot_code'
High-level design
*Configuration Service* - This is a micro service responsible for providing admin api s as described above . The domain entities it maintains are ParkingLot and ChargePolicy
*User Interface* - a UI built over Configuration service and Ticketing Service.
*Ticketing Service* - this microservice provides api for the checkin and checkout flows. Having this service separate from Configuration Service due to its different scaling and reliability needs. Additionally it can also provide validate ParkingSlot Entry api.
*ReadService* - This microservice powers the api Vacancy Summary . This api returns the current count of available slots per size type for a given slot . This api powers various electronic display boards that are refreshed at a higher frequency
Request flows
*Checkin Flow* -- api called -- POST /parkingLots/
- Request body --
{
"vehicle_reg_num": "KA-03-XX-1234",
"size_type":"C" ,
"current_entry_epoch_time_min": 1735551
}
- TicketService uses the ParkingSlot table as a priority queue to find a parking slot of the required size or closest to it by using the following query :
update ParkingSlot
set is_empty=false , vehicle_reg_num='KA-03-XX-1234' , current_entry_epoch_time_min=1735551
where
lot_code="LOT1"
and is_empty=true
and size_type_enum_C_S_L >= C
order by size_type_enum_C_S_L ASC
limit 1
returning slot_code
- In the above query , vehicle_reg_num + current_entry_epoch_time_min is used as idempotency check . As there is a composite unique key constraint defined on these 2 keys , in the event of a client retry this query will fail with a uk_constraint violation error , which should be handled in TicketService code . This exception is an indiciation that the current request is a retry/duplicate one and in that case instead of trying to create ticket , the application should lookup and return the ticket.
- On success of the above step , an insert to the Ticket table is performed with the allocated slot_code to create the ticket and get the ticket id.
- On success of the above 2 steps , ticket generation event is created and written to a outbox table
- The above 3 db query/statement execution should happen in a db transaction such that the appropriate atomicity and isolation are guaranteed
Detailed component design
*Read Service* - This service keeps the summary ( size_type --> count ) per Parking lot code in memory to be able to provide a very low latency response to the vacancy Summary api
- On the service startup and also on partition reallocation , the instances fetch the current state ( lot_code --> vacancy_summary ) and caches in memory.
- They also subscribe to the "Ticket Event Topic"
- On receiving Ticket created and Ticket closed events it updates the size type count values inaccordingly on the vacancySummary stored in its in-memory cache.
- The read threads read the vacancySummary from in-memory cache and respond
- Ticket Event topic is partitioned by parking lot code . Thus events for a given parkign lot is processed by the same instnce of ReadService , this enables the Data locality to keep the count in memory.
- On the request path , the vacancySummary api request on load balancer can be routed based ont he parkingLot code in it is url . A custom plugin can be written for the load balancer ( e.g kong ingress for k8s ) so that lb 's routing algorithm can track the kafka partition allocation and route the request to the same instance to which the requested parking lot's events are listened and summary is maintained .
Trade offs/Tech choices
- Instead of using Postgresql table ParkingSlot for the slot allocation logic ( used as a priority queue ) - redis could have been used . However using redis had the following Pros and Cons -
- Pros - lower latency at lower throughput .
- Cons -
- Need additional infrastructure( extra cost ) to be provisioned for a narrow usecase ..
- Need additonal code to populate the cache on startup / initialization and shard maintenance .
- At very high load , especially in hot partition scenario where a particular parking lot is getting significantly more traffic then the rest , it is scales poorly .
- As redis operation do not take part in distributed transaction , the atomicity and isolation guarantees ( strong consistency ) needed between the slot allocation and ticket generation is difficult to provide .
Failure scenarios/bottlenecks
- Network failure between client and server during the check in or check out process --> The api s are designed to be idempotent , thus the client can safely retry. Currently the idempotency key in check-in flow is :- vehicle_reg_num + epoc_time_min . It is expected that the client does not change epoch_time_min during retry for the same action.
- TicketService is stateless and can be scaled up based on its resource usage to support high load
- Postgresql is run with a master-slave configuration so when the master fails on of the secondaries ( slaves ) can be immediately promoted ( after it has caught up with all events ) .
- On the read path , the kafka broker runs with high availability guarantee .
- Read service is replicated , however , in the current approach it can be scaled only up to the number of parking lots. This can be improved by using technology like kafka streams and partitioning the Ticket Event topic to lower granularity than the parking lot code
Future improvements
- Removing dependency of a centralized postgresql and opting for total event driven architecture and leveraging in-memory data structure and horizontal scaling for better scalability.
- Modelling user entity rather than using headless user for more richer experience to the user .
- Leveraging Kafka Streams based aggregation on "Ticket Event" partitioned in a more fine grained fashion - the current partitioning scheme of Ticket Event topic is vulnerable to hot partition problem .