Ride-sharing is one of the most frequently asked system design interview questions at top tech companies, and for good reason. It combines real-time data processing, geospatial indexing, distributed matching algorithms, and financial transactions into a single problem. If you can design Uber convincingly, you have demonstrated mastery over patterns that apply to dozens of other systems.
This article walks through the complete 6-stage framework applied to designing a ride-sharing service. We will cover every stage from requirements through scaling, with the depth that interviewers at companies like Google, Meta, and Amazon expect from senior candidates.
Stage 1: Requirements Gathering (3-5 Minutes)
Start by converting the open-ended prompt into a concrete specification. Do not touch the whiteboard until you have established what you are building, how big it is, and what trade-offs you are willing to make.
Functional Requirements
A strong requirements conversation might begin like this:
"I want to make sure I understand the core use cases before designing. The essential flows are: a rider requests a ride from point A to point B, the system matches them with a nearby available driver, both parties track each other in real time during the trip, the system computes an ETA and fare estimate upfront, and payment is processed automatically when the ride completes. Should I also include ride pooling, scheduled rides, or driver ratings in scope?"
For a 45-minute interview, scope down to the core: ride request, driver matching, real-time tracking, ETA computation, and payment processing. Mention pooling and scheduling as extensions you would add later.
Non-Functional Requirements
"For scale, I will target a system serving a large metropolitan market. Let us say 1 million active drivers updating their location every 4 seconds, which gives us 250,000 location updates per second. On the ride side, 10 million rides per day, or roughly 115 rides per second at steady state, with 5-10x peaks during rush hour. Matching latency should be under 5 seconds from ride request to driver notification. Location updates should propagate to riders within 1-2 seconds. The system must be highly available — a rider stranded because the app is down is unacceptable. We can tolerate eventual consistency for non-critical data like ride history."
These numbers are critical. They tell the interviewer you understand the problem's scale characteristics: location updates are the write-heavy hot path, ride requests are the latency-sensitive critical path, and everything else is secondary.
Back-of-the-Envelope Math
- Location updates: 1M drivers x 1 update/4s = 250K writes/second
- Ride requests: 10M/day = ~115/s average, ~1,000/s peak
- Location data size: Each update is ~100 bytes (driver_id, lat, lng, timestamp, status). At 250K/s, that is 25 MB/s or 2.16 TB/day of raw location data
- Active trip tracking: At any given time, roughly 500K concurrent rides, each requiring real-time location push to the rider
Stating these numbers explicitly demonstrates quantitative thinking that separates senior candidates from junior ones.
Stage 2: API Design (3-5 Minutes)
Define the external contracts before designing internals. The ride-sharing system has two distinct client types — riders and drivers — each with different API needs.
Rider APIs
POST /v1/rides/request — Request a new ride. Request body: { "rider_id": "r_123", "pickup": { "lat": 37.7749, "lng": -122.4194 }, "dropoff": { "lat": 37.3382, "lng": -121.8863 }, "ride_type": "standard" }. Response: { "ride_id": "ride_abc", "estimated_fare": { "min": 24.50, "max": 32.00, "currency": "USD" }, "estimated_pickup_eta": 240, "status": "matching" }. Requires authentication.
GET /v1/rides/{ride_id}/status — Poll trip status and driver location. Response includes status (matching, driver_en_route, arrived, in_progress, completed), driver location, and updated ETA. Alternatively, push updates via WebSocket.
POST /v1/rides/{ride_id}/cancel — Cancel a ride. Applies cancellation fee if driver already dispatched.
Driver APIs
PUT /v1/drivers/{driver_id}/location — Update driver location. Request body: { "lat": 37.7751, "lng": -122.4183, "heading": 45, "speed": 12.5, "timestamp": 1710244800 }. Called every 4 seconds. Must handle 250K requests/second globally.
POST /v1/rides/{ride_id}/accept — Driver accepts a ride request. Must respond within 15 seconds or the request is forwarded to the next driver.
POST /v1/rides/{ride_id}/update — Driver updates trip status (arrived at pickup, started trip, completed trip). Request body: { "status": "pickup_arrived" }.
Internal APIs
POST /v1/matching/find — Internal service call to find the best available driver for a ride request. Takes pickup location, ride type, and returns ranked list of candidate drivers with ETAs.
Notice how writing these APIs surfaces key design decisions: should trip tracking be poll-based or push-based? (Push via WebSocket for real-time experience.) Should location updates be fire-and-forget or acknowledged? (Fire-and-forget with UDP-like semantics — losing one update out of every 250K per second is acceptable.)
Stage 3: Data Model (3-5 Minutes)
The ride-sharing system has four core entities with very different access patterns and storage requirements.
Trips Table
| Field | Type | Notes |
|---|---|---|
| trip_id | UUID (PK) | Partition key for point lookups |
| rider_id | UUID (FK) | Secondary index for ride history |
| driver_id | UUID (FK) | Null until matched; indexed |
| status | ENUM | matching, driver_en_route, arrived, in_progress, completed, cancelled |
| pickup_location | POINT | lat/lng |
| dropoff_location | POINT | lat/lng |
| estimated_fare | DECIMAL | Computed at request time |
| actual_fare | DECIMAL | Computed at completion |
| created_at | TIMESTAMP | |
| completed_at | TIMESTAMP |
Storage choice: a relational database (PostgreSQL) works well for trips. The access patterns are straightforward — point lookups by trip_id, range queries by rider_id for history, and status filtering for operational dashboards. At 10M new rows per day, a single PostgreSQL instance handles writes comfortably; partition by month for long-term manageability.
Drivers Table
| Field | Type | Notes |
|---|---|---|
| driver_id | UUID (PK) | |
| name | VARCHAR | |
| vehicle_type | ENUM | standard, xl, premium |
| rating | FLOAT | Rolling average |
| status | ENUM | offline, available, en_route, on_trip |
| current_location | POINT | Latest known position |
The driver profile lives in PostgreSQL, but the real-time location and availability data demands a different approach. Updating a database row 250,000 times per second is not viable. Driver locations belong in an in-memory geospatial index — this is the critical data model decision for this problem.
Location Data (Hot Path)
Driver locations are ephemeral, high-frequency, and accessed spatially ("find drivers within 3 km of this point"). This is not a traditional database workload. The right abstraction is an in-memory geospatial index that supports two operations efficiently:
- Update: Insert or update a driver's position — 250K/s
- Query: Find all available drivers within radius R of point P — hundreds/s, but latency-critical
Options include Redis with its built-in geospatial commands (GEOADD, GEORADIUS), a custom in-memory quadtree, or a geohash-based index. We will explore these in the deep dive.
Riders Table
Standard user profile table in PostgreSQL. Nothing unusual — rider_id, name, email, payment_method_id, rating. Low-frequency reads and writes.
Key Insight
The data model for ride-sharing is split across two fundamentally different storage paradigms: durable relational storage for trips, users, and payments (correctness matters, write volume is moderate) and ephemeral in-memory storage for real-time location data (speed matters, data is constantly overwritten). Recognizing this split and articulating why is a strong signal in the interview.
Stage 4: High-Level Architecture (5-8 Minutes)
Walk through each major flow separately. Every box you draw should serve a purpose you can explain.
Flow 1: Ride Request
The rider opens the app, enters pickup and dropoff, and taps "Request Ride." Here is the sequence:
- The Rider App sends
POST /v1/rides/requestto the API Gateway - The Trip Service creates a new trip record with status
matching, computes estimated fare based on distance and current surge multiplier, and returns the trip_id to the rider - The Trip Service publishes a
ride_requestedevent to the Matching Service - The Matching Service queries the Location Service for available drivers within a configurable radius (start with 3 km, expand to 5 km if insufficient candidates)
- The Matching Service ranks candidates by ETA, rating, and acceptance rate, then sends a ride offer to the top-ranked driver via the Notification Service
- The driver has 15 seconds to accept. If they decline or timeout, the next driver is notified. After 3 rejections, the radius expands
- When a driver accepts, the Trip Service updates the trip status to
driver_en_routeand notifies the rider with driver details and ETA
Flow 2: Location Updates
This is the highest-throughput path in the entire system. Every active driver sends a location update every 4 seconds.
- The Driver App sends
PUT /v1/drivers/{id}/locationto the API Gateway - The Location Service updates the driver's position in the geospatial index (in-memory, backed by Redis)
- If the driver is currently on a trip, the Location Service pushes the updated position to the rider via WebSocket through the Notification Service
- The raw location data is asynchronously written to a time-series store for analytics, ETA model training, and regulatory compliance
The key insight: location updates must not hit the primary database. They flow through the in-memory geospatial index only. The database stores the latest known position as a denormalized field, updated asynchronously, not on every 4-second tick.
Flow 3: Trip Completion and Payment
- The driver taps "Complete Trip" — the Driver App sends
POST /v1/rides/{id}/updatewith statuscompleted - The Trip Service computes the actual fare based on the recorded route, distance, duration, and surge multiplier
- The Trip Service publishes a
trip_completedevent to the Payment Service - The Payment Service charges the rider's payment method, computes driver payout (after platform commission), and records the transaction in the ledger
- Both rider and driver receive a trip summary via the Notification Service
Stage 5: Deep Dive (8-10 Minutes)
This is where the interview is decided. We will go deep on two areas: geospatial indexing (the foundation of the entire system) and the driver matching algorithm.
Deep Dive 1: Geospatial Indexing
The core query is: given a pickup location, find all available drivers within N kilometers. This must return results in under 100 milliseconds while handling 250,000 location updates per second. Three approaches are worth discussing.
Approach A: Geohash-Based Index
A geohash encodes a two-dimensional coordinate into a one-dimensional string by recursively subdividing the Earth's surface into a grid. A 6-character geohash represents a cell roughly 1.2 km x 600 m. A 5-character geohash covers approximately 5 km x 5 km.
How it works: when a driver sends a location update, compute their geohash at precision 6 and store it in a hash map keyed by geohash cell. When a rider requests a ride, compute the rider's geohash and query the driver's cell plus all 8 adjacent cells (to handle boundary effects). Filter results by exact haversine distance.
Advantages: Simple to implement, works naturally with Redis (GEOADD/GEOSEARCH use geohash internally), prefix-based queries enable flexible precision. Disadvantages: Cells are not uniform in size near the poles (not a problem for ride-sharing in practice), edge effects require querying neighboring cells, fixed grid does not adapt to driver density.
Approach B: Quadtree
A quadtree recursively divides a 2D space into four quadrants. Unlike geohash, the tree adapts to data density: densely populated areas (downtown Manhattan) subdivide into smaller cells, while sparse areas (rural highways) remain as large cells. Each leaf node holds up to K drivers (e.g., K=100).
How it works: insert drivers into the tree as they update their location. Remove them from their old cell and insert into the new one. To find nearby drivers, traverse from the root to the leaf containing the pickup location, then expand to neighboring leaves until enough candidates are found or the radius is exhausted.
Advantages: Adapts to density (efficient in both dense cities and sparse suburbs), search is O(log N). Disadvantages: More complex to implement, the tree must be rebuilt or rebalanced as drivers move, not natively supported by Redis.
Approach C: S2 Geometry (Google's Approach)
S2 maps the Earth's surface onto the faces of a cube, then uses a Hilbert curve to convert 2D coordinates to 1D cell IDs. This preserves spatial locality better than geohash and provides uniform cell sizes. Uber uses a variant called H3 (hexagonal hierarchical geospatial index).
Practical Recommendation for the Interview
Use Redis with its built-in GEOADD and GEOSEARCH commands. This gives you a geohash-based index with no custom implementation. For driver updates: GEOADD drivers:available {lng} {lat} {driver_id}. For nearby queries: GEOSEARCH drivers:available FROMLONLAT {lng} {lat} BYRADIUS 3 km ASC COUNT 20. When a driver becomes unavailable (accepts a ride, goes offline), remove them: ZREM drivers:available {driver_id}.
To handle 250K updates/second, shard the Redis cluster by city or region. Each city has its own Redis instance or cluster. A ride request in San Francisco only queries the San Francisco shard. This partitioning is natural because ride-sharing is inherently local — a driver in New York will never match with a rider in Los Angeles.
Mention that Uber's production system uses a custom in-memory service called Ringpop built on top of a SWIM gossip protocol for distributed geospatial state management. This kind of reference demonstrates awareness of how real companies solve these problems at extreme scale.
Deep Dive 2: Driver Matching Algorithm
Finding nearby drivers is necessary but not sufficient. You need to select the best driver from the candidate set. This is a constrained optimization problem.
Naive Approach: Nearest Driver
Sort candidates by straight-line distance, pick the closest. This is fast but suboptimal: the closest driver by Euclidean distance might be on the other side of a highway with no nearby exit. A driver 500 m further away but on the same road could arrive 5 minutes sooner.
Better Approach: ETA-Based Ranking
For each candidate driver, compute the actual driving-time ETA from their current location to the pickup point using a routing engine. Rank by ETA rather than distance. This accounts for road networks, traffic conditions, and one-way streets.
ETA computation is expensive — you cannot call a routing engine for 50 candidate drivers on every ride request. The optimization: use the geospatial index to find the nearest 20 drivers by straight-line distance (cheap), then compute ETAs only for those 20 (expensive but bounded). This two-phase approach limits routing engine calls while maintaining match quality.
Production Approach: Multi-Factor Scoring
Real ride-sharing systems score drivers on multiple factors:
| Factor | Weight | Rationale |
|---|---|---|
| ETA to pickup | 0.40 | Rider experience — shorter wait is always better |
| Driver rating | 0.15 | Match quality — higher-rated drivers provide better experience |
| Acceptance rate | 0.15 | Reliability — drivers who frequently decline waste time |
| Driver heading | 0.10 | A driver heading toward the pickup is better than one heading away |
| Trip completion rate | 0.10 | Reduces cancellations after matching |
| Idle time | 0.10 | Fairness — prioritize drivers who have been waiting longer |
The matching service computes a composite score: score = w1 * normalize(eta) + w2 * rating + w3 * acceptance_rate + ... and selects the driver with the highest score. The weights are tunable and can be adjusted by city, time of day, or ride type.
Dispatch Strategy: Single vs. Batch
The simplest dispatch is single dispatch: for each ride request, find the best driver and send them an offer. If they decline, try the next one. This is straightforward but slow — worst case, you cycle through several drivers, adding 15 seconds per attempt.
A more efficient approach is batch dispatch: accumulate ride requests for a short window (2-3 seconds), then solve a bipartite matching problem to optimally assign multiple riders to multiple drivers simultaneously. This is what Uber and Lyft use in practice. The Hungarian algorithm or auction-based algorithms can find the global optimum, but even a greedy approximation outperforms sequential single dispatch. The trade-off: batch dispatch adds 2-3 seconds of latency to every request but produces better global matches and fewer driver rejections.
Stage 6: Scaling and Trade-offs (5 Minutes)
Step back and evaluate where your design breaks, what trade-offs you made, and how you would evolve the system.
Location Update Throughput
250K location updates per second is the defining scalability challenge. Our solution — city-sharded Redis clusters — handles this well because ride-sharing is geographically partitioned. Each city shard handles 5-50K updates/second depending on city size, well within Redis's throughput ceiling of 100K+ operations/second per node.
If a single city outgrows one Redis node (unlikely but possible for a megacity like Sao Paulo), further shard by geographic quadrant within the city. The key insight: location data has natural spatial partitioning that maps cleanly to horizontal scaling.
Surge Pricing
When demand exceeds supply in a geographic area, increase prices to incentivize more drivers to that area. Implementation: divide each city into hexagonal zones (H3 cells). For each zone, maintain a demand/supply ratio updated every 30 seconds. When the ratio exceeds a threshold (e.g., 1.5x), apply a surge multiplier to fares in that zone. The surge multiplier is computed as a function of the demand/supply ratio, capped at a maximum (e.g., 5x). Expose the current surge map to drivers so they can see where demand is high.
The tricky part: surge pricing creates a feedback loop. High prices reduce demand and attract supply, which should lower the surge. The system must dampen oscillation — do not drop the multiplier immediately when supply increases, or you get a ping-pong effect. Use exponential decay with a minimum hold time of 5 minutes.
ETA Computation
Accurate ETAs require a routing engine that accounts for real-time traffic. At scale, you cannot call Google Maps for every ETA computation — it is too slow and too expensive. Build an internal routing service using OpenStreetMap road graph data with real-time traffic overlays from your own driver GPS data. Pre-compute shortest paths between frequently queried origin-destination pairs. Use historical trip data to build machine learning models that predict travel time based on time of day, day of week, weather, and events.
Cache ETAs aggressively: the ETA from geohash cell A to geohash cell B does not change significantly within a 5-minute window during non-peak hours. This reduces routing engine load by 90%+.
Multi-Region Deployment
Ride-sharing is inherently regional. A rider in London will never match with a driver in Tokyo. This means you can deploy independent regional stacks with no cross-region dependencies for the real-time path. Each region has its own location service, matching service, and Redis cluster. Share user profiles, payment, and analytics globally via asynchronous replication.
For cities near region boundaries, assign each city to exactly one region. A driver physically near a boundary operates in the region their city is assigned to.
Monitoring and Operational Metrics
Four critical metrics to monitor:
- Match time (p50, p95, p99): Time from ride request to driver acceptance. Alert if p95 exceeds 60 seconds — indicates driver supply shortage or matching service degradation
- Location update lag: Time between driver's GPS reading and its availability in the geospatial index. Alert if p99 exceeds 3 seconds — riders would see stale driver positions
- ETA accuracy: Compare predicted ETA at match time with actual pickup time. Track mean absolute error. If error exceeds 3 minutes consistently, the routing model needs retraining
- Cancellation rate: Both rider and driver cancellations. A spike indicates matching quality issues or surge pricing backlash
Scoring Tips for Ride-Sharing
This problem tests three skills more than most system design questions:
- Geospatial thinking: If you cannot explain geohash or quadtree mechanics, you will stall on the core technical challenge. Practice drawing geohash grids and explaining the neighbor-cell query pattern
- Real-time systems intuition: The interviewer wants to hear you reason about 250K writes/second as a natural consequence of the requirements, not as an afterthought. Derive the number from first principles during Stage 1
- Matching as optimization: Going beyond "pick the nearest driver" to multi-factor scoring and batch matching is what separates Senior from Staff-level answers
Common mistakes to avoid: designing the payment system in too much detail (it is important but not the core challenge), forgetting to handle the driver rejection/timeout flow (what happens when the first driver does not accept?), and treating location updates as a database write workload instead of an in-memory one.
Practice This Problem
Reading a walkthrough builds familiarity, but fluency comes from practice under realistic conditions. Hoppers AI offers system design mock interviews that guide you through all six stages with AI-driven follow-up probes — the same format top companies use. After each session, you receive a detailed scorecard on requirements clarity, architecture quality, technical depth, and communication, so you know exactly where to improve.