Total bookings in a day = 2 * 10^5 = ~2 bookings / sec
Peak bookings = ~10 bookings / sec
Scale = 10 countries -> 100 parking lots per country with 2-3 times a day per spot
Storage:
User Info - Unique ID, Name, email address, ID type, ID value ~ 500 byte / user
Total bookings in a day = 200K
Total bookings in a month = 200K * 30 = 6*10^6
An average user parks 3-4 times a month
Unique user per month = 1.5*10^6
So, user storage comes at 1.5*10^6*500 byte/ month = 750MB
Booking info - Unique ID, User, location, in_time, duration, payment_status = 200 byte
Total storage in a day = 2* 10^5 * 200 byte /day = 40MB/day = 75 GB in 5 years
GET /parking_spot/availability/{parking_lot_id}
Response:
{
available_big_slot: int,
available_small_slot: int,
available_ev_slot: int
}
POST /parking_spot/booking
Request:
{
user_id: uuid,
parking_lot_id: uuid,
parking_spot_id: uuid,
car_type: ENUM,
check_in_time: timestamp,
check_out_time: timestamp,
idempotency_key: uuid
}
Response:
{
success: True,
booking_id: uuid,
payment_status: ENUM (Completed, Pending etc)
}
POST /bookings/check_in
Request:
{
user_id: uuid,
booking_id: uuid,
check_in_time: timestamp
duration: int (minutes)
}
Response:
{
success: True,
check_in_time: timestamp
}
POST /bookings/check_out
Request:
{
user_id: uuid,
booking_id: uuid,
check_out_time: timestamp
}
Response:
{
success: True,
check_out_time: timestamp,
pending_payment: double (in case of overstay, or additional charges on top of booking amount)
}
POST /bookings/delete
Request:
{
user_id: uuid,
booking_id: uuid,
}
Response:
{
success: True,
amount deducted: double
}
POST /bookings/modify
Request:
{
user_id: uuid,
booking_id: uuid,
check_in_time: timestamp,
duration: int (minutes),
idempotency_key: uuid
}
Response:
{
success: True,
amount: double (could be negative if there is a refund)
}
POST /bookings/payment
Request:
{
user_id: uuid,
booking_id: uuid,
amount: double,
idempotency_key: uuid
}
Response:
{
success: True
}
In case payment is unsuccessful, we can ask the user to retry the payment again. If we do not receive the payment within the stipulated time, the booking will be deleted.
The clients calls Load Balancer, which calls API Gateways, which directs the request to the Application Servers.
We have primarily 4 Services.
Availability Service:
This Service primarily serves the data from Redis. We cache the data in Redis, and on booking/hold/cancellation, we invalidate the cache.
This is required to speed up the Availability Service.
Booking Service:
This is our core service. It updates the Postgres DB with the booking Details, and updates the Redis cache (This is an async process, where we put the event in a queue, and another worker will update the cache).
Payment Integration:
We integrate 3rd party vendors for Payment, once the payment is completed, it calls our Booking Service, which updates the payment status in DB.
Booking Service also has a message queue, where we will dump events for sending notification / pending Payments / Invalidating Redis cache etc.
Parking Lot Service:
This is used to mark the check-in & check-out of the user, which calls Booking Service to persist the details to DB.
We will have cron jobs running in the background, checking for currently active bookings (whose check in time + grace period > current time) with no show and add them to the queue for cancelling those bookings
We have primarily 2 DB:
We need atomic updates, the write speed is moderate and the data fits a structured pattern. Postgress DB fits perfectly for our usecase.
We will have the following tables:
We will shard the data based on lot_id, so all spots and reservations of that lot stays in 1 shard.
Redis:
We will store the information about the available slots.
Key -> (lot_id, spot_type), Value -> int
Key -> spot_id_availability -> boolean
We currently update the Redis asynchronously, once the slot is held or booking is done.
This leaves a small window, where a slot might be booked, but Redis shows it as available.
Postgres is our ultimate source of truth, so, when we try to book an already taken slot, it will show users a simple msg - "The slot is just taken, please try another slot". Since the write speed is moderate, this will be a trade off between simple write path and fast reads compared to showing slightly stale data to users. We can later make the Redis update synchronous.
We will deep dive into following things:
once a booking is initiated, we check the Postgres DB as source of truth, if the spot is currently free, we add an entry for the slot and add an event to the queue to asynchronously update the redis. If an entry already exists, we will not book, and show an error msg to the user.
For entering a booking into the db, we will put a lock based on spot_id, so that 2 users can't book it at one go
We store Redis keys as availability:lot_id:spot_type which gives us the total counts of the spots available in the current lot for that vehicle type (here in the logic, we will let smaller vehicle park in bigger slots, if they are ready to pay for that).
We have another entry, which shows the booking status of a spot_id.
Redis is updated async, so the data might be slightly stale, but we accept this trade-off for faster reads and writes.
We will accept overlapping intervals (end == start).
If there is an hold on a key, we will have a background job, that continuously scans for bookings whose status is not CONFIRMED, and the created_time + locking_period < current time and mark those entries as CANCELLED and release the hold