A few items that require estimation
For queries per second, we'd divide 200 by 100,000 seconds. Leaving us with less 1/100 of a request per second
2/1000 request a second basically. Which gets to 1/500 requests per second. If we asume with our calculations below, that means around 1/5 bytes per second, which is not even worth considering for data transfer.
If we have 1000 parking lots across 10 countries and 200 reservations per lot per day, then we have around 200,000 requests/reservations per day.
Let's assume that each reservation requires the following information
1) Lot ID -> 8 bytes
2) Customer ID -> 8 Bytes
3) Date -> 4 bytes
4) Time reservation starts -> 4 bytes
5) Amount paid -> 4 bytes
6) Vehicle Type -> 1 byte (bool of isCar)
7) Duration -> 4 bytes
8) Left_early -> 1 byte
9) Late_to_reservation -> 4 bytes
10) No_show -> 1 byte
Let's just estimate on the far end that each customer requires 100 bytes of data (even though it falls short).
In that case, if we have 200,000 requests per day, then we'd have 20,000,000 bytes per day or 20MB per day. That means we require 365* 20 MB per year which is ~7000 MB which is ~7GB per year. Let's assume that we want to be on the safe side, so let's allocate x3, leaving us with ~21GB per year.
In terms of throughput, there's not much that we actually need to maintain. Assuming 200 reservations per day, that's at most 20MB per day transferred over network, which is negligible.
In terms of the API design, I think there are a few APIs that we will have to support
POST /api/v1/reservation
This would be the API call that actually tries to reserve the spot for the user.
which would have the following data
{
lot_id
customer_id
date
reservation_day_start
reservation_time_start
reservation_day_end
reservation_time_end
vehicle_type
}
I think we'd need to have a start/end day and start/end time because the reservation can last multiple days. In that case, if we don't have end day, then we won't be able to represent that.
It would return an error code. 0 if the reservation was successful. There would be other error code that represent different error scenarios such as "failed to obtain reservation" or "reservation already claimed", etc.
GET /api/v1/available_spots
{
lot_id
date
reservation_day_start
reservation_time_start
reservation_day_end
reservation_time_end
vehicle_type
}
This would return the available spots in the specified parking lot given the start/end times. We need to specify start/end day/time because a person may have already reserved a spot for a different start time that overlaps with the specified start/end time.
GET /api/v1/reservation
This would return the specified reservation details
{
user_id
reservation_id
}
PUT /api/v1/reserve_spot
{
user_id
lot_id
spot_id
reservation_day_start
reservation_time_start
reservation_day_end
reservation_time_end
}
reserves a spot for a set duration (let's say 1 hour). If the user does not checkout/purchase the reservation, then it is released back to the pool of available spots
At a high level, I think there are a few considerations to be made.
1) Multi-region support
To support multiple countries and regions, we need to have a multi-region datacenter configuration. This does add the complication of replication across datacenters to ensure strong consistency. But with the small # of queries per day, we can utilize synchronous replication across read replicas in the database and across datacenters.
2) At a high level, the client will make a request. The request will first go through a rate limiter to throttle excessive requests/DDOS attacks. The request will then pass to the load balancer which will route the request to one of the servers. The server will handle all requests from the client and either query the database for the relevant information and return it to the client (reservation info, available spots + available times, etc). It will also be in control of reserving spots for the user when they haven't checked out (locking the relevant rows in the database with that lease lasting one hour). Lastly, it'll be the one that will send POST transactions to the database when the user does checkout.
3) We would require a separate service to actually handle the payment. The server would engage with this service when the user commits to purchasing the reservation. When this goes through, the server would then commit the transaction on the database as well. We want these both to be atomic. In general, I think we first try committing to the database first and then go and commit the transaction on the payment service. This way, if the database transaction fails, we don't charge the customer. If the database transaction succeeds, then we continue with the payment charge. If that fails, we rollback the database commit.
I'd considered having it as the reverse, but the payment transaction is usually a third party service that does not allow for rollbacks. So once we commit there, then it'd be difficult to 'undo.
One last thing is that we can also add a cache between the server and the database. For any 'hotspot' queries, we can handle it at the cache level rather than querying the database directly. We'd generally keep the cache TTL relatively low (around 1-2 hours) in case the user queries availability and another user may have cancelled, thus changing the response.
parking_lot(lot_id, capacity, address, contact_info)parking_spot(spot_id, lot_id, spot_type, is_available)reservation(reservation_id, user_id, lot_id, spot_id, start_time, end_time, status, payment_status)lot_id so all spots/reservations for a lot stay co-locatedlot_id and spot_id for fast queryingDeep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
So let's first get the payment processor out of the way first. Here'd I'd go with a 3rd party trusted component to handle the payment processing due to the complexity of properly handling payments. We'd have to deal with chargebacks, validation of the actual payment, etc. I think it's worth it to pay a small fee to have another service handle it for us. We'd just query for the relevant payment amount and then receive an error code (if there is an error) and handle it accordingly.
The other aspects we'd want to handle is the actual flow of the data and how the GET/POST requests work.
Let's first quickly go through the GET request once it gets to the server.
Let's simulate the "Ask available spots"
The user inputs
lot_id
date
reservation_day_start
reservation_time_start
reservation_day_end
reservation_time_end
vehicle_type
1) Server queries the database and will have it look at the reservation table.
It specifically looks for any spots that do not fall within the specified start_day/start time and the end_day/end_time.
2) The server returns all the relevant spots that fit that criteria.
Let's then go into the actual POST reservation
1) User makes a request to reserve a spot_id for a specified start time/date and end_day/end_time.
2) Server checks whether the parking_spot is actually available (and not locked).
3) If it isn't, then the server requests a lease for an hour for that entire parking spot for the start and end_day. We could lock more granually, but with the number of requests, we can do a more general locking mechanism per spot.
4) If the user doesn't commit within an hour ,then the lease expires and the lock is released on the relevant row.
5) If the user does commit, then the server first commits to the database and creates a new row in the reservation table. For now, it continues maintaining the lock (in case it needs to rollback).
6) When the server receives an ACK for the operation, then the server initializes a payment request to the payment service. The payment service tries to commit the payment amount and charge to the provided payment method.
7) If the payment goes through, then the server releases the lock on the database and sends a success response to the client.
8) If the payment fails, then the server will have the database rollback the transaction on the reservations table and then send a failure response to the client.
Lastly, I think one thing that'd be worth deliberating is the replication across datacenters. In general, I don't expect users to be making requests for a spot from a different country. So users should be making reservations while in the same country. This way, we don't need to make use of asynchronous replication for cross-regional data centers. Otherwise, we'd have to wait for ACKs from the other datacenters which leads to complications since we would have to potentially deal with lost responses, network partitions, etc. In terms of the read replicas in the same datacenter, we'd want at least a quorum of replicas to be replicated synchronously. This way, when reads are performed, they don't have to be done on the master/leader. instead, they can be spread across the readers.
We'd also need some way to guarantee monotonic reads. If the user queries for their reservation history and have just made a reservation and they see their new reservation. If they refresh and lose their new reservation, then that becomes a problem. Instead, we need to have some sort of "versioning" for reads so that reads get routed to replicas that have at least that 'version' of data. For the most part, if we always route to a synchronous replica, then this won't happen.
In terms of potential areas we could also work on, we'd probably want some sort of logging/analysis service in case errors and or analytics are needed.
Tradeoffs/failures