We use cookies for analytics to improve our service. See our Privacy Policy.

    Sign up free to unlock interview prep materials and a free mock interview for your next role.

    Start Free
    system design
    question breakdown
    ride sharing

    Design a Ride-Sharing Service

    Hoppers AI Team·March 12, 2026·12 min read

    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

    FieldTypeNotes
    trip_idUUID (PK)Partition key for point lookups
    rider_idUUID (FK)Secondary index for ride history
    driver_idUUID (FK)Null until matched; indexed
    statusENUMmatching, driver_en_route, arrived, in_progress, completed, cancelled
    pickup_locationPOINTlat/lng
    dropoff_locationPOINTlat/lng
    estimated_fareDECIMALComputed at request time
    actual_fareDECIMALComputed at completion
    created_atTIMESTAMP
    completed_atTIMESTAMP

    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

    FieldTypeNotes
    driver_idUUID (PK)
    nameVARCHAR
    vehicle_typeENUMstandard, xl, premium
    ratingFLOATRolling average
    statusENUMoffline, available, en_route, on_trip
    current_locationPOINTLatest 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.

    Rider AppDriver AppAPI GatewayTrip ServiceLocation ServiceGeospatial IndexMatching ServicePayment ServiceNotification ServiceTrips DBRedis ClusterDriver QueueStripe / LedgerPush / SMSWebSocket connections for real-time location push to Rider & Driver apps(via Notification Service)Message Queue (Kafka / SQS) — decouples trip events, analytics, payment processing

    Flow 1: Ride Request

    The rider opens the app, enters pickup and dropoff, and taps "Request Ride." Here is the sequence:

    1. The Rider App sends POST /v1/rides/request to the API Gateway
    2. 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
    3. The Trip Service publishes a ride_requested event to the Matching Service
    4. 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)
    5. 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
    6. The driver has 15 seconds to accept. If they decline or timeout, the next driver is notified. After 3 rejections, the radius expands
    7. When a driver accepts, the Trip Service updates the trip status to driver_en_route and 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.

    1. The Driver App sends PUT /v1/drivers/{id}/location to the API Gateway
    2. The Location Service updates the driver's position in the geospatial index (in-memory, backed by Redis)
    3. If the driver is currently on a trip, the Location Service pushes the updated position to the rider via WebSocket through the Notification Service
    4. 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

    1. The driver taps "Complete Trip" — the Driver App sends POST /v1/rides/{id}/update with status completed
    2. The Trip Service computes the actual fare based on the recorded route, distance, duration, and surge multiplier
    3. The Trip Service publishes a trip_completed event to the Payment Service
    4. The Payment Service charges the rider's payment method, computes driver payout (after platform commission), and records the transaction in the ledger
    5. 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:

    FactorWeightRationale
    ETA to pickup0.40Rider experience — shorter wait is always better
    Driver rating0.15Match quality — higher-rated drivers provide better experience
    Acceptance rate0.15Reliability — drivers who frequently decline waste time
    Driver heading0.10A driver heading toward the pickup is better than one heading away
    Trip completion rate0.10Reduces cancellations after matching
    Idle time0.10Fairness — 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.