Library Uber / Ride Sharing AgileViper46
Uber / Ride Sharing — system design by AgileViper46 Lean Hire Reviewed by 6 specialized AI reviewers. Explore the diagram and the full per-section feedback below.
Author's explanation How the author described their design For all the apis the authentication will be done and the user id will be extracted from the JWT not from request body
1. Getting the fare estimate
- Riders will be calling the fare service and it will be using the google maps api to calculate the time, distance and then based on the information it will be caculating the fare price and store in the db.
- To speed this up and to lower the external api depenednecy and cost we will cache this distance, time in the location cache with a ttl of 10 mins (example). We will not be storing the exact lat, long but the geohash for the lat, long. Using this if any subsequent request comes we can use the cache which saves the api cost and estimation is much faster.
2. Users can request the ride
- Users with the given fare can request the ride.
- Upon request firstly the ride service will validate the fare id is the same and valid fare for the ride calculated by this user and fare is not stale (cannot use 2 hour older fare).
- Then its going to create a new ride in the DB with the status as created.
- Upon creation of ride the user will be calling the /updates endpoint which will create a SSE connection useful for showing info to users like searching for drivers (expected 5 mins for drivers, driver location update etc...)
3. Ride matching
- when a new ride is created we can use the AWS semantics for the dynamoDB streams and push that to amazon sqs. The ride matching service will take this new ride and try to match this with the drivers.
- The ride matching service will first select drivers lets say with 3km of radius using the location cache. This can be done using the redis geohash query.
- One by one the ride matching service will send the notification to the driver until one driver accepts. but if multiple ride matching instance sends notification to same driver to multiple ride it will not be good. So the ride matching service will first do a distributed locking using SETNX and only one instance will send the notification to the driver. This will be done with the TTL so that if the matching instance dies with holding lock then this will be released.
- To send the request it will find which WS server the driver is connected to and then send the request to the driver.
- Upon the request driver can do yes | no. In case of no the matching service will cache this ride_id_driver_id in the db so that it does not send the same notification again if the instance crashes and other takes up or if it starts a new search with advanced 5km radius.
- If driver accepts then ride matching service updates the ride table with the driver id and status as matched. This is done using the dynamoDB transaction where it checks if the driverId is assigned and the status is matched then someone beat us and we fail this and show that someone else accepted
4. Driver location update
- The driver device will be continously sending the update message for the location based on the regular internvals like 10-20 seconds. If the driver is not connected to the ws server its marked offline.
- the lcoation service will update the location of the driver in the DB with the status and also the location cache.
- The ride service can read this cache to send the update about the driver location for the ride.
Fialure considerations
- Fare and ride service will have the idempotency key for durable retries.
- each ws server can handle 20k connection and we have 1M drivers so we need 50 servers
- The drivers will be connected to the ws servers using consistent hashing so that if the driver reconnects they assing to same server.
- Considering 1M drivers sending update every 10 Seconds that means 100K RPS just for location updates.
- Instead of storing every update in DB we can just put the location in the DB every 2 minute.
- Also the client side probing can be made dynamic based on the gps changes. If the user is in traffic and not moving much we can send updates every 30 secs or even 1 minute to reduce the footprint.
- 500K users if everyone request ride at the same time we need 50 Ride service servers.
- From the DB side to handle 500K write and assuming one partion can handle 500 wps that means we need 1000 partitions in the DB. we can partition the data by the userId
- Enforce single-driver acceptance with a conditional write
Use one atomic DynamoDB write (UpdateItem or TransactWriteItems) with a condition such as:
attribute_not_exists(driverId) AND status = 'SEARCHING'
Do not use a read-then-write flow. The first accepting driver succeeds; all late acceptances fail automatically.
Rider uniqueness
GSI is for lookup, not concurrency protection
Keep a separate active-ride guard item:
PK = RIDER#<riderId>
Create this item conditionally (attribute_not_exists(PK)) in the same transaction that creates the ride. Delete it when the ride completes or is canceled. This guarantees at most one active ride per rider even under concurrent requests.
WebSocket resilience
Add server-side presence with TTL heartbeats
Do not rely only on client reconnects. Store presence in Redis:
driverId -> serverId (with TTL)
Each WS server refreshes the TTL every few seconds. If a server dies, the keys expire quickly, and the matcher stops routing requests to stale connections. Client reconnect then re-registers the driver on a healthy server.
AI
Design Review Hire Signal Lean Hire
The candidate demonstrates strong instincts on core distributed-systems concerns and solves the most important correctness races with concrete mechanisms, which is a meaningful senior-level positive. The main reason this is not a full hire is that several critical production paths remain under-specified end to end—especially reliable eventing and scalable rider update fanout—so the design is good but not yet fully convincing for the stated scale.
AI Review Senior (L5-L6) Expand All⭐ Excellent Consistency requirements are explicitly differentiated by workflow
The design clearly separates where strong consistency is required versus where eventual consistency is acceptable: single-driver assignment and one active ride per rider are treated as correctness-critical, while driver location updates are allowed to be eventually consistent. That is the right NFR framing for this problem because it ties consistency choice to user-visible failure modes like double assignment.
✅ Good Concrete latency targets are stated for key user flows
The candidate gives specific targets for fare estimation (<1s) and driver matching query (<200ms) instead of vague goals like 'fast'. This is useful because it creates measurable expectations for the two most latency-sensitive parts of the system.
✅ Good Some scale numbers are connected back to assumptions
The explanation uses the stated peak assumptions to reason about load, such as 1M online drivers leading to roughly 100K location-update RPS at 10-second intervals and sizing WebSocket capacity from 1M driver connections. That shows the NFRs are not entirely floating in isolation.
warning Availability versus consistency trade-off is stated but not reconciled
You say 'Availability >> consistency', but the same design also requires strong guarantees for ride assignment and one active ride per rider. What happens during a partition or datastore impairment when you cannot safely enforce those invariants? The system must either reject/queue ride accepts and ride creation or risk duplicate assignments. Call out which operations sacrifice availability and which can degrade gracefully.
warning Latency targets are not fully defended against external dependency risk
Fare estimation has a <1s target, but it depends on Google Maps unless there is a cache hit. What happens when the external API is slow or rate-limited and the request is a cache miss? Without an explicit degraded-mode NFR or fallback behavior, the latency target is not really defensible under normal failure scenarios.
warning Matching latency target is ambiguous at the user-experience level
The design gives '<200ms' for the driver matching query, but the actual rider-facing flow includes candidate lookup, notification delivery, driver response time, retries, and expanding radius. What happens if the query is fast but the rider still waits minutes for a match? It would be stronger to distinguish internal service latency from end-to-end ride-request SLA so the NFR reflects the real experience.
info Availability targets are qualitative rather than measurable
You identify availability as important, but there is no concrete target such as uptime/SLO for fare estimation, ride request, or live location updates. You could improve this by defining service-level objectives per flow, especially because different flows tolerate different degradation levels.
info Scale assumptions are only partially tied to the stated NFRs
You connect some throughput numbers to peak users, but the NFR section does not close the loop on whether those loads still meet the stated latency goals. You could improve this by explicitly saying, for example, that the 100K location-update RPS is acceptable because location is eventual, while ride creation and assignment paths are provisioned to preserve the stronger correctness and latency requirements.
🗃️ Core Entities Review
✅ Good Core nouns for the main ride flow are identified
The design names the main domain objects needed for the stated flow: User, Driver, Ride, Fare, and DriverLocation. That is enough to trace estimate -> request ride -> driver accepts -> rider sees driver location without inventing extra product features.
✅ Good Ride is treated as the central stateful entity
The explanation makes Ride the anchor for the workflow, with status transitions like created and matched plus driver assignment. That gives the system a clear source of truth for the request/accept lifecycle rather than scattering state across unrelated components.
warning User and Driver relationship is underspecified
Have you considered whether Driver is a separate entity or a role/profile attached to a User? Without making that relationship explicit, identity and lifecycle questions become fuzzy: what happens when the same person is both a rider and a driver, or when auth gives you a userId but ride assignment needs a driverId?
warning Fare-to-ride linkage is implied but not clearly modeled
What happens when a rider requests a ride using an old or mismatched estimate? You mention validating fare id, ownership, and staleness, which is good, but the entity relationship itself is not clearly stated. It would strengthen the model to explicitly define whether a Ride references exactly one Fare estimate and whether a Fare belongs to one User and one source/destination pair.
warning DriverLocation relationship to active rides is not explicit
Have you considered how DriverLocation connects to the rider-facing live location view? Right now it reads like DriverLocation belongs to a Driver, but for the happy path you also need the relationship from Ride -> assigned Driver -> latest DriverLocation to be explicit, otherwise the read path for 'show me my driver's live location' is left implicit.
info Rejected match attempts appear as data but not as an entity/relationship
You could improve this by explicitly modeling the relationship between Ride and candidate Drivers during matching. Since you already store ride_id_driver_id to avoid re-notifying the same driver, calling out that a Ride can have many driver offers/attempts would make the matching flow easier to reason about.
✅ Good Peak write rate estimated for driver location updates
The candidate converts 1M online drivers with 10-second updates into roughly 100K location-update RPS and then uses that to justify reducing durable writes by only persisting every 2 minutes. That is the right capacity-planning instinct: start from peak actors and event frequency, then reduce database pressure by separating hot ephemeral state from long-term storage.
✅ Good Connection capacity sizing for WebSocket tier
They explicitly size the WebSocket layer using a per-server connection budget of 20K and derive about 50 servers for 1M drivers. Even if the exact per-node number is debatable, the methodology is sound and shows they are thinking in terms of concurrent connections rather than only request QPS.
warning Capacity chain is only partially developed
You have a few isolated numbers, but have you considered carrying the full chain from 10M DAU and 1.5M concurrent users through to peak fare-estimate QPS, ride-request QPS, match attempts per ride, storage growth, and network bandwidth? Without that end-to-end model, it is hard to tell whether the proposed infrastructure is balanced or whether one tier becomes the bottleneck first.
warning Read-heavy live location fanout is not sized
What happens when 500K riders are simultaneously watching live driver location while 1M drivers are publishing updates? You estimated ingest at 100K RPS, but the rider-facing read/fanout side is often the larger load. A stronger capacity plan would estimate update frequency to riders, outbound message volume, and bandwidth on the SSE/WebSocket tier.
warning Database partition estimate is disconnected from access patterns
You mention 500K writes and derive 1000 partitions at 500 WPS each, but have you considered what those writes actually are and whether partitioning by userId fits the hottest write paths? Ride creation, active-ride guard items, driver assignment, and location writes may concentrate on different keys. Without mapping workload to partition keys, the partition count does not yet prove the database will avoid hot partitions.
warning No storage growth or retention sizing
Have you considered how much data accumulates over time for rides, fare estimates, driver presence, and periodic location snapshots? Even if exact numbers are rough, senior-level capacity planning should show whether this is gigabytes, terabytes, or more per day and what retention assumptions drive database and backup cost.
info Include peak amplification during matching
You could improve this by estimating how many candidate drivers are contacted per ride request and translating that into notification throughput. Matching often amplifies one rider request into many downstream operations, so sizing only the ride-request path can understate the true peak load on queues, caches, and connection servers.
info Justify infrastructure choices with load shape
You could strengthen the capacity section by tying component choices to scale more explicitly: for example, why DynamoDB is appropriate for the expected write burst pattern, why Redis can hold the active geospatial working set, and why SQS is sufficient for matcher throughput. Right now the components are named, but the scale-based justification is still thin.
✅ Good Core rider and driver flows are mostly covered
The API set supports the main functional path in the requirements: fare estimation via POST /fares, ride creation via POST /rides, driver acceptance over WebSocket, and rider-facing live updates over a streaming channel. The candidate also ties authentication to JWT-derived identity instead of trusting user IDs from the body, which is the right API boundary.
⭐ Excellent Idempotency called out for ride creation and retries
Using an Idempotency-Key on POST /rides is a strong choice for a mobile network environment where clients retry aggressively. Combined with the explanation about durable retries and conditional writes, this shows awareness of duplicate ride creation and race conditions at the API boundary.
✅ Good Protocol split matches interaction patterns
Using request/response HTTP for fare lookup and ride creation, WebSocket for driver push/response, and a server-push stream for rider updates is a reasonable protocol mix for this use case. It avoids forcing polling for driver dispatch and location updates.
warning Rider updates endpoint is protocol-confused
POST /rides/:id/updates --> SSE is not a usable API as written. SSE is a server-to-client streaming response, so what happens when the rider wants to subscribe to updates after creating a ride? A cleaner contract would be something like GET /rides/:id/updates for SSE, or a WebSocket subscription message if you want bidirectional behavior. As written, 'rider->server for ride updates' conflicts with how SSE works.
warning Missing explicit driver accept API/message contract
The design says RideResponse() over WebSocket, but what exactly does the client send when accepting a ride, and how does it correlate to a specific request? What happens if the driver taps accept after the ride was already claimed by someone else? Define message types and payloads clearly, e.g. ride.request, ride.accept, ride.reject with rideId/requestId, so late or duplicate accepts can receive a deterministic failure response.
warning Error handling is underspecified for client behavior
What does the client see when a fare is stale, the fare ID does not belong to that rider, the ride was already created under the same idempotency key, or no driver accepts? Without clear status codes or WebSocket/SSE error events, mobile clients cannot distinguish retryable failures from terminal ones. Add a consistent error shape and retry guidance for HTTP and streaming protocols.
info Ride resource design could be more resource-oriented
You could improve this by making the rider-facing API more explicit around the ride resource itself, for example GET /rides/:id for current status and GET /rides/:id/updates for streaming updates. Right now the design relies heavily on the stream, but a normal fetch endpoint helps reconnect/recovery when the client misses events.
info Fare creation response should make request-to-book flow explicit
You could strengthen the API contract by clarifying that POST /fares returns a fareId plus expiry metadata, so the client knows how long the estimate is valid before calling POST /rides. The explanation mentions stale fare validation, but the API surface should expose that lifecycle directly.
⭐ Excellent Thought through ride-assignment concurrency
The design explicitly handles the hardest correctness path in this problem: preventing multiple drivers from winning the same ride and preventing one rider from creating multiple active rides. Using a lock to limit duplicate notifications plus a conditional/transactional DynamoDB write for final assignment shows good awareness that matching is a distributed race, not a simple read-then-write flow.
✅ Good Hot-path location data is separated from primary storage
Keeping driver locations in Redis for nearest-driver lookup and live location reads is a solid architectural choice at the stated scale. It avoids pushing high-frequency location traffic onto DynamoDB and keeps the matching and rider update paths low latency.
✅ Good WebSocket routing includes connection-to-server mapping
The design does not treat the WS tier as a black box; it includes driverId-to-serverId presence mapping so the matcher can route ride requests to the correct WS node. That is an important end-to-end detail many candidates miss in real-time systems.
✅ Good Fare estimation path uses cache to reduce external dependency pressure
Caching geohash-based route estimates in front of Google Maps is a practical optimization for both latency and cost. It is relevant to the <1s fare estimate target and shows awareness that third-party APIs can become the bottleneck.
critical Ride creation to matching flow is not reliably end-to-end
What happens if the DynamoDB-to-SQS handoff is delayed, duplicated, or fails for some ride events? The rider would see a ride created but matching may never start, or the same ride may be matched multiple times. Since the whole request path depends on DB streams feeding SQS, you need a clearly reliable eventing story here: idempotent consumers keyed by rideId, retry/dead-letter handling, and a way to detect and recover rides stuck in SEARCHING with no matcher progress.
warning Nearest-driver search architecture is underspecified at 1M online drivers
Have you considered what happens when a large city has very dense driver concentration and many concurrent ride requests hit the same Redis geo index? Redis location lookup becomes the first hot spot in this design because every match request depends on it. You mention Redis geohash query, but not how the location cache is partitioned, replicated, or isolated by region/city. At this scale, a single global location cache is likely to become both a throughput bottleneck and a blast-radius problem.
warning Live driver location delivery path to riders is incomplete
What happens after a driver is matched and starts sending location updates? The design says Ride Service reads Redis and sends SSE updates, but it does not show what triggers those rider updates. If Ride Service polls Redis per rider connection, that becomes expensive with hundreds of thousands of riders. If updates are pushed from Location Service, that fanout path is not shown. You need a clear push model or subscription model for matched rides so rider updates do not devolve into large-scale polling.
warning WebSocket cluster still has a failure-mode gap during server loss
Have you considered what happens to in-flight ride offers when a WS server dies after the matcher routes a request to it but before the driver receives or responds? Presence TTL cleanup helps future routing, but current offers can be silently lost and matching latency spikes. The design would be stronger if ride offers had explicit delivery/ack timeouts and automatic re-dispatch when the WS node or driver session disappears mid-offer.
info Some components and connections are doing double duty without clear ownership
You could improve this by tightening component responsibilities. For example, Redis DB and the Location Service both appear involved in driver-to-server mapping, and the WS cluster also points back to Ride Matching with the same mapping annotation. That makes it harder to reason about the source of truth and failure handling. A cleaner ownership model would be: WS servers own presence registration, Redis stores ephemeral presence, matcher reads presence, and Location Service owns only location ingestion.
info DynamoDB is being used for both transactional ride state and periodic location persistence
You could improve this by separating the write-heavy location history/status persistence path from the transactional ride state path. Even if you only flush location every 2 minutes, mixing these concerns in one store can create noisy-neighbor effects and complicate partition design. Keeping ride state isolated makes the critical assignment path easier to scale and reason about.
Want this kind of feedback on your own design? Draw your architecture for Uber / Ride Sharing and get an instant hire/no-hire signal from 6 specialized AI reviewers — free to start.