+ Users should be able to request a ride
+ Drivers should be able to take rides
+ Users should be able to pay for their ride
+ Authentication
+ Abuse detection
+ Consistency
+ Availability
+ Latency is a second priority
Out of scope:
+ Logging
+ Analytics
+ Enrollment
The supply for drivers is limited (hundreds per region).
The demand for drivers can be much larger (thousands).
We should expect millions of drivers and tens of millions of users.
(user) /api/v1/cost [GET]
req: {
current location
destination
}
response: {
expected wait time
cost
}
(user) /api/v1/ride [POST]
req: {
current location,
destination,
type of ride
}
response (streaming): {
driver location
driver status
}
(driver) /api/v1/get_next_ride [GET]
req: {
current location
}
response: {
(list) ride {
ride id
cost
destination
}
}
(driver) [POST ]/api/v1/accept_ride/
req{
ride id
driverid
}
response {
status
}
Drivers Table
DriverId (Index)
Name
Picture Path
Score
Status (Riding/Not riding)
Users table
UserId (Index)
Name
Picture Path
Payment details
PaymentId (Index)
UserId
Card details
Preferred?
Rides
RideId
DriverId
UserId
Start Location
Destination
CreatedTime
Drivers locations Table (Vector DB or Geospatial table)
Location (lat, long) (index)
DriverId
we have an orchestration server (web server for taking API requests).
The server redirects and calls multiple microservices depending on the type of request.
On the database side, we rely on some relational DB, like spanner or MySQL. For the drivers location table we need a specific geospatial type of database, so we can query using location type. The shards are created by location center.
Users maintain a streaming connection when requesting rides. Drivers update two different things, their location in the table and their location to the users.
every time drivers update their location, we can send an update to the users. only 1/5 times we need to update it in the DB.
If a connection is lost, we can just try to re-enable it using the rideid.
Different user journeys:
1) Drivers are using the app.
A streaming connection is started
The location is sent to the orchestration server.
The os calls the rides service to update the drivers location table. If the driver is not in a ride, post the update. If the driver is on a ride (this only happens 1/5 times). There's a few ways to ensure that it happens 1/5 times, either we trust the client to count this or we maintain state on the server side. In order to maintain state, we can either make sure that each request is routed to the same server or we can keep a separate storage mechanism. On a separate side, the rides service will issue a notification to the user.
2) Users want to know the cost of a ride. The rides service will compute the distance cost (baseline), and will apply some multiplier based on the number of available drivers.
3) Users request a ride:
users send a request to the orchestration service. the os will call the rides service. the rides service will create an entry in the rides table, without userid.
the response comes back to the user, with a rideid that they can use to know when there's an available ride.
in the background there's an ongoing process that offers rides to drivers, we call this the offer cron job. once a rider accepts a ride, we use the driver id in the rides table.
since the user is constantly querying the status of their ride, they will at some point get the driver id.
once users have a driver id, they will call the rides service to getdriverslocation. we can either maintain state and try to ensure that users can reach the same server or just let the request fan out all the way down to the database. I prefer letting the request slide all the way down to the database, since the number of requests per second is pretty low 1 request every 3 seconds per driver and one request every 3 seconds per user.
the rides service will query `x` available drivers. Then it will offer the drives to each of these drivers, one by one. when a driver accepts a ride.
in the background we have a payments service (also a cron job), that checks the status of completed rides. this service will try to use the default method of payment to pay for the last ride. if the payment didn't succeed, we should update the status of the user to something like "debt pending", which won't allow them to take more ride. on the signup flow, we should be a little more cautious when letting people sign up. we can do this by doing identity verification, etc...
Explain any trade offs you have made and why you made certain tech choices...
what happens when multiple users try to use the app and there are no drivers available?
we should let users know such case and ask them to retry. an alternative is to show a UI that says "trying to get your driver" or something along those lines, although, i think from a UX experience. users can't tell the source of the problem.
what happens when a user doesn't pay?
we would block their account so that they can't ask for any more rides.
how can we make sure that a driver doesn't take two rides?
we should have some guarding from the UI. when a driver accepts a ride, the internal UI should go into a state machine that goes into "finalizing details of the ride". on the server side, we can prevent this by updating the driver table and changing their status, we can use pessimistic concurrency control since a driver wouldn't update their status that often (once every x minutes).
What are some future improvements you would make? How would you mitigate the failure scenario(s) you described above?