Assumption: Support 1000 cities across globe & no cross region operation support
Initial numbers:
Derived numbers for single city:
Peak traffic:
Assume 20% users become active at same time.
Booking traffic: 20,000 active users * 1 request/minute = 333 requests/second
Browsing traffic: 20,000 active users * 10 request/minute = 3333.3 requests/second
For all API requests, 401 on unauthorized or 404 on unknown endpoint or 400 on bad request.
GET /events
Response: Event[]
GET /events?filter=&sort_by=&pageLimit=&pageSize=
Response: Event[]
GET /events/{eventId}
Response: Event
GET /events/{eventId}/shows/{showId}/seats
Response: Seat[]
POST /bookings
Body: {
"event_id": int,
"seats": Seat[],
}
Response: {
"booking_id": int
}
POST /
NOTE: No view bookings endpoint as not part of the functional requirements.
For booking, we re-direct the user to payment link after reserving the seat for some TTL(10 minutes). User can retry the payment too and that is part of payment system. After payment is confirmed and the request is sent to our internal booking service, we reserve the seat for user after confirming TTL has not expired. TTL is auto released through redis expiry or background job. We will decide later.
For each endpoint, individual flows are drawn. All external facing requests are sent through API gateway for authentication and authorization. We can double down on load balancer too as part of this component in future.
Event
Venue
Show
Seat
Booking
Considering our avg traffic and their peaks, SQL and NoSQL can both support the read/write ratios. What would be deciding factor over here is the data consistency guarantees needed.
For Event, Venue & Show, they don't have strict consistency or atomicity guarantees needed, so document Db or key value Db works best for them.
For Seat, requirements are:
Such kind of reservation is best served by Redis as it is single threaded and provide key expiry per TTL. We keep the reservation_state in Redis and auto expire keys created in 10 min. Whenever a request comes in, first we check in Reservation Db(Redis) and then create entry in it if not present for reserving. Since lock is acquired, no one else will write to that row. Seat Db itself should be Sql as we need atomic writes on Seat db's reservation state using conditional write on state being VACANT when payment is done. So in essence when serving seat map, we need to exclude all BOOKED seats as well as reserved seats in redis. For doing this, we can store all entries for a event's show in single HashMap.
For Booking, we need ACID guarantees, so Sql Db.
All shards are by mapped by show_id.
When request comes in for seat map, we have seats that are BOOKED and seats that are on HOLD for reservation. To serve them, first it gets the non booked seats from Seat Db, and then gets locks in Reservation db's Hashmap for that specific show and filters out the present seats in Redis.
For hot events like Taylor swift concert, we will have too high resource contention for individual seats.
Either we scale up the backend resources to serve all of those requests or only serve specific requests.
Remembering our original requirement of consistency over availability for booking, we have to reject request even if SeatMap showed non-vacant seat as vacant.
We will use hybrid approach here: Keep total left seat count for hot shows in API gateway specified using header when returning the response during the seat map request. We will also scale up Bookings service as it is stateless. We will add read replicas to Seat Db as read requests are multiple and final BOOKED seats will only be some 100k rows with only one way change usually. So for a given show writes will only be 100k for seats and can be easily handled by single node of Psql. For reads, we will just use read replicas to serve the traffic.
Now, we still have to compute left over seats from Redis and build that. We can reverse this and just send the reserved seats to users and make client side compute the left seats. So in essence, the user will get seat map for event as part of event details. While getting event seatmap, we can forward union of BOOKED seats in seat Db and locked seats from Redis as they are non-conflicting seats and even if repeats are present, doesn't harm the rendered seat map on user side.
If the payment fails, then user should still be allowed to retry payment later. So we will have to introduce retry payment API which forwards to Booking service and then replays the original scenario after confirming lock is still held by the user.