system-design47 min read

Design Uber: The Complete Ride-Sharing System Design Guide - A Senior+ Guide | Ayodhyya

Design Uber: The Complete Ride-Sharing System Design Guide

The definitive senior+ engineering guide to building a production-grade ride-hailing platform from first principles

Senior+ Guide 45+ min read 10,000+ words Ayodhyya

Table of Contents

  1. Introduction - Why Ride-Sharing Is a Premier System Design Problem
  2. Functional and Non-Functional Requirements
  3. Capacity Estimation and Back-of-Envelope Math
  4. High-Level Architecture Overview
  5. Location Ingestion Pipeline and Geohash Indexing
  6. Proximity Search - Finding Nearby Drivers in Real-Time
  7. Matching Engine - From Ride Request to Driver Assignment
  8. Surge Pricing - Dynamic Fare Calculation
  9. Trip Lifecycle and State Machine
  10. Real-Time Trip Tracking with WebSockets
  11. Payment Processing and Fare Settlement
  12. Data Storage Architecture and Sharding Strategy
  13. Safety, Fraud Detection, and Anomaly Handling
  14. Caching, CDN, and Performance Optimization
  15. Multi-Region Deployment and Disaster Recovery
  16. Monitoring, Observability, and SLA Management
  17. Cost Estimation and Infrastructure Sizing
  18. Complete C# Implementation - Matching and Trip Services
  19. Interview Q&A - 25 Questions with Detailed Answers
  20. Conclusion

1. Introduction - Why Ride-Sharing Is a Premier System Design Problem

Designing a ride-sharing system like Uber is one of the most frequently asked system design interview questions at FAANG companies, and for good reason. A ride-hailing platform sits at the intersection of almost every major distributed systems concept: real-time geospatial data ingestion, proximity search using spatial indexing, event-driven architecture, dynamic pricing, state machine management, real-time communication via WebSockets, payment processing, and global multi-region deployment. It is the quintessential generalist system design problem because it touches infrastructure, algorithms, databases, messaging, and user experience in a single coherent domain.

Uber operates in over 10,000 cities across 72 countries, completing approximately 25 million trips per day with a fleet of over 5 million drivers. The platform processes over 40,000 location pings per second at peak, serves ride requests with sub-200ms matching latency, and manages dynamic pricing that updates every 30 seconds per geohash cell. These numbers are not aspirational; they are the production reality that any serious system design must address. Building even a simplified version of this system forces you to reason about trade-offs between consistency and availability, latency and throughput, and correctness and scale.

In this comprehensive guide, I walk through the complete architecture of a ride-hailing platform from the ground up. We start with requirements gathering, estimate scale, design the data model, build the location ingestion pipeline, implement the matching engine with geohash-based proximity search, design the surge pricing system, manage trip state machines, process payments, and handle failure at every layer. Every section includes C# code blocks, HTML tables for comparison, and Mermaid diagrams for architecture visualization. By the end, you will have a production-grade blueprint that demonstrates senior+ engineering judgment.

Interview Context: The ride-sharing design question tests your ability to reason about real-time systems, geospatial data structures, event-driven architectures, and stateful workflows. It is commonly asked at Uber, Lyft, Amazon (for logistics), Google (for Maps), and Microsoft (for Azure Maps). Expect 45-60 minutes for a thorough walkthrough.

2. Functional and Non-Functional Requirements

Functional Requirements

#RequirementPriorityDetails
F1Rider requests a rideMustRider specifies pickup/dropoff, selects ride type, sees fare estimate
F2Driver location trackingMustDrivers send GPS pings every 3-4 seconds, stored and indexed in real-time
F3Driver-rider matchingMustMatch nearest available driver within configurable radius using proximity search
F4Surge pricingMustDynamic multiplier based on supply-demand ratio per geohash cell
F5Real-time trip trackingMustRider sees driver ETA and live location on map during trip
F6Trip lifecycle managementMustState machine: REQUESTED, ACCEPTED, IN_PROGRESS, COMPLETED, PAID
F7Payment processingMustSupport credit card, digital wallets, and split fare
F8Driver availability toggleMustDriver can go online/offline, affecting real-time availability index
F9Ride historyShouldBoth rider and driver can view past trips with routes and receipts
F10Rating systemShouldBidirectional rating: rider rates driver, driver rates rider
F11Safety featuresShouldSOS button, trip sharing, driver verification, PIN-based pickup
F12Multi-ride-type supportNiceEconomy, premium, pooled, wheelchair accessible vehicle options
F13Scheduled ridesNiceRider books a ride in advance for a future time
F14InternationalizationNiceMulti-currency, multi-language support across regions

Non-Functional Requirements

RequirementTargetRationale
Matching latency< 200ms (p99)Riders expect near-instant driver assignment
Location ping ingestion< 50ms (p99)Real-time driver tracking requires sub-second writes
Availability99.99%Downtime means zero rides, directly impacting revenue and safety
ConsistencyEventual (location), Strong (trip/payment)Location data can be stale; trip state and money cannot
Throughput40K location writes/sec, 42K match QPSPeak demand in dense urban areas
Data retentionTrip history: 5+ years, Location: 90 daysRegulatory and business requirements
Geo-redundancyActive-active multi-regionDatacenter failure must not take down the platform
Fare accuracyEstimate within 10% of finalUser trust depends on predictable pricing

3. Capacity Estimation and Back-of-Envelope Math

Daily Volume Estimates

MetricCalculationResult
Daily active ridersGiven10 million
Daily active drivers (peak)Given1 million
Daily tripsGiven15 million
Location pings per driver per day86,400 sec / 4 sec interval21,600 pings
Total location pings per day1M drivers x 21,60021.6 billion pings
Location ping payloaddriver_id + lat + lng + timestamp + metadata~500 bytes
Daily location data ingress21.6B x 500 bytes~10.8 TB/day
Average QPS (ride requests)15M / 86,400~174 requests/sec
Peak QPS (ride requests, 10x)174 x 10~1,740 requests/sec
Peak QPS (matching, with retry)1,740 x 3 driver candidates~5,220 match QPS
Location write QPS (peak)500K drivers / 4 sec~125,000 writes/sec
Daily trip storage15M x 2 KB per trip~30 GB/day
Annual trip storage30 GB x 365~11 TB/year
Annual location storage10.8 TB x 90 days retention~972 TB (archived)

Storage Breakdown by Service

ServiceStorage TypeDaily GrowthRetention
Location historyCassandra (time-series)10.8 TB90 days
Trip dataPostgreSQL (sharded)30 GB5+ years
User profilesPostgreSQL / MongoDB~1 GBIndefinite
Real-time locationsRedis (ephemeral)~60 GB (in-memory)10 sec TTL
Driver earningsPostgreSQL~5 GBIndefinite
Payment transactionsPostgreSQL + Kafka~2 GB7 years (regulatory)
Analytics eventsKafka to ClickHouse~500 GB1 year
Key Insight: The location ingestion pipeline is the highest-throughput component, handling 125K+ writes per second at peak. This is 3 orders of magnitude higher than the ride request QPS. The system design must prioritize the location write path and use asynchronous processing for non-critical paths like analytics.
C#
public class CapacityEstimator
{
    private const int DailyActiveRiders = 10_000_000;
    private const int DailyActiveDrivers = 1_000_000;
    private const int DailyTrips = 15_000_000;
    private const int PingIntervalSeconds = 4;
    private const int PingPayloadBytes = 500;
    private const int TripRecordBytes = 2_048;

    public void CalculateScale()
    {
        int pingsPerDay = (86400 / PingIntervalSeconds) * DailyActiveDrivers;
        long locationBytesPerDay = pingsPerDay * PingPayloadBytes;
        double locationTBPerDay = locationBytesPerDay / (1024.0 * 1024 * 1024 * 1024);

        double avgRideQPS = DailyTrips / 86400.0;
        double peakRideQPS = avgRideQPS * 10;
        double peakLocationWriteQPS = DailyActiveDrivers / (double)PingIntervalSeconds;

        long tripStoragePerDay = DailyTrips * TripRecordBytes;
        double tripGBPerDay = tripStoragePerDay / (1024.0 * 1024 * 1024);

        Console.WriteLine($"Location pings per day: {pingsPerDay:N0}");
        Console.WriteLine($"Location data per day: {locationTBPerDay:F1} TB");
        Console.WriteLine($"Avg ride QPS: {avgRideQPS:N0}");
        Console.WriteLine($"Peak ride QPS: {peakRideQPS:N0}");
        Console.WriteLine($"Peak location write QPS: {peakLocationWriteQPS:N0}");
        Console.WriteLine($"Trip storage per day: {tripGBPerDay:F1} GB");
    }
}

4. High-Level Architecture Overview

The ride-sharing platform consists of several core services orchestrated through an event-driven architecture. The key services are: Location Service (ingests and indexes GPS pings), Matching Service (pairs riders with drivers), Trip Service (manages trip lifecycle), Pricing Service (computes fare estimates and surge), Payment Service (processes payments), Notification Service (push notifications and SMS), and the Rider/Driver API Gateway that serves client requests. These services communicate asynchronously through Kafka for event-driven workflows and synchronously via gRPC for low-latency queries.

graph TB subgraph Clients RA[Rider App] DA[Driver App] end subgraph Gateway Layer LB[Load Balancer] RAG[Rider API Gateway] DAG[Driver API Gateway] WSG[WebSocket Gateway] end subgraph Core Services LS[Location Service] MS[Matching Service] TS[Trip Service] PS[Pricing Service] PayS[Payment Service] NS[Notification Service] end subgraph Data Layer Redis[(Redis Cluster)] PG[(PostgreSQL)] Cassandra[(Cassandra)] Kafka[Kafka] end subgraph External Maps[Google Maps API] PaymentGW[Payment Gateway] FCM[Firebase Cloud Messaging] end RA --> LB DA --> LB LB --> RAG LB --> DAG LB --> WSG RAG --> TS RAG --> PS DAG --> LS DAG --> MS WSG --> LS LS --> Redis LS --> Cassandra LS --> Kafka MS --> Redis MS --> Kafka TS --> PG TS --> Kafka PS --> Kafka PayS --> PG PayS --> PaymentGW Kafka --> MS Kafka --> TS Kafka --> NS NS --> FCM TS --> Maps

Service Responsibilities

ServiceProtocolResponsibilityLatency Target
Location ServicegRPC / WebSocketIngest GPS pings, update Redis, persist to Cassandra< 50ms
Matching ServicegRPCFind nearest drivers, score candidates, send ride requests< 200ms
Trip ServicegRPC / RESTCreate trips, manage state machine, calculate fares< 100ms
Pricing ServicegRPCCompute surge multiplier, fare estimates< 100ms
Payment ServiceRESTCharge rider, pay driver, handle refunds< 500ms
Notification ServiceAsync (Kafka)Push notifications, SMS, email< 5s
User ServiceRESTAuth, profiles, driver documents< 100ms

Communication Patterns

The architecture uses two primary communication patterns. First, synchronous gRPC for latency-sensitive queries: the rider app calls the Trip Service to create a ride request, the Matching Service queries Redis for nearby drivers, and the Pricing Service returns a fare estimate. These calls must complete within 200ms and use circuit breakers with fallbacks. Second, asynchronous Kafka events for eventual consistency workflows: when a trip is completed, the Trip Service publishes a TripCompleted event that triggers the Payment Service to charge the rider, the Notification Service to send receipts, and the Analytics pipeline to record metrics. Kafka provides durability, ordering guarantees, and decoupling between producers and consumers.

C#
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpcClient<MatchingService.MatchingServiceClient>(o =>
        {
            o.Address = new Uri("http://matching-service:5000");
        });
        services.AddGrpcClient<PricingService.PricingServiceClient>(o =>
        {
            o.Address = new Uri("http://pricing-service:5001");
        });
        services.AddGrpcClient<LocationService.LocationServiceClient>(o =>
        {
            o.Address = new Uri("http://location-service:5002");
        });

        services.AddSingleton<IKafkaProducer>(sp =>
        {
            var config = new ProducerConfig
            {
                BootstrapServers = "kafka-cluster:9092",
                Acks = Acks.All,
                EnableIdempotence = true,
                LingerMs = 5,
                BatchSize = 16384
            };
            return new KafkaProducer(config);
        });

        services.AddStackExchangeRedisCache(o =>
        {
            o.Configuration = "redis-cluster:6379,abortConnect=false";
        });
    }
}

5. Location Ingestion Pipeline and Geohash Indexing

The location ingestion pipeline is the backbone of the entire ride-sharing system. Every driver on the platform sends GPS coordinates every 3-4 seconds, generating over 21 billion pings per day across the global fleet. These pings must be ingested, validated, indexed for real-time proximity queries, and persisted for historical analysis. The pipeline must handle massive write throughput while maintaining low latency so that driver locations remain fresh in the proximity index.

Geohash Explained

A geohash is a hierarchical spatial encoding that converts latitude and longitude into a short alphanumeric string. The key insight is that geohashes with a common prefix are spatially proximate. At precision level 6, a geohash cell covers approximately 1.2 km x 0.6 km, which is a useful granularity for urban ride-matching. At precision 7, the cell shrinks to approximately 153m x 153m, providing neighborhood-level accuracy. The matching service uses precision 7 as the primary lookup and expands to precision 6 and 5 when more candidates are needed.

Location Ping Flow

sequenceDiagram participant Driver participant WebSocket GW participant Location Service participant Kafka participant Redis participant Cassandra loop Every 3-4 seconds Driver->>WebSocket GW: GPS Ping (lat, lng, speed, bearing) WebSocket GW->>Location Service: gRPC WriteLocation Location Service->>Redis: ZADD drivers:active geohash6 Location Service->>Redis: SET driver:current driverId EX=10 Location Service->>Kafka: Publish LocationEvent Kafka->>Cassandra: Async persist location history end

Geohash Precision Levels

PrecisionCell Size (approx)Use CaseDrivers per Cell (NYC)
Level 439 km x 20 kmCity-level partitioning~50,000
Level 55 km x 5 kmDistrict-level matching~8,000
Level 61.2 km x 0.6 kmNeighborhood matching~500
Level 7153m x 153mPrecise proximity search~15-50
Level 838m x 19mStreet-level (too fine)~1-3

Redis Data Structure for Driver Locations

The real-time location index uses Redis sorted sets where the key is the geohash prefix at level 6, the member is a composite string containing the driver ID and full geohash, and the score is the timestamp of the last ping. This design enables two critical queries: (1) find all drivers in a specific geohash cell using ZRANGEBYSCORE, and (2) expire stale drivers by removing entries older than 10 seconds using ZREMRANGEBYSCORE. The current driver location for a specific driver is stored in a separate Redis key with a 10-second TTL for O(1) lookups by driver ID.

C#
public class LocationIngestionService : ILocationIngestionService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly IKafkaProducer _kafkaProducer;
    private readonly IDatabase _db;
    private const int GeohashPrecision6 = 6;
    private const int GeohashPrecision7 = 7;
    private const int StaleDriverTTLSeconds = 10;

    public LocationIngestionService(
        IConnectionMultiplexer redis,
        IKafkaProducer kafkaProducer)
    {
        _redis = redis;
        _db = redis.GetDatabase();
        _kafkaProducer = kafkaProducer;
    }

    public async Task IngestLocationPingAsync(LocationPing ping)
    {
        if (!IsValidCoordinate(ping.Latitude, ping.Longitude))
            throw new ArgumentException("Invalid GPS coordinates");

        string geohash7 = Geohash.Encode(ping.Latitude, ping.Longitude, GeohashPrecision7);
        string geohash6 = Geohash.Encode(ping.Latitude, ping.Longitude, GeohashPrecision6);
        long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

        string member = $"{ping.DriverId}:{geohash7}";
        string sortedSetKey = $"drivers:active:{geohash6}";

        var tasks = new List<Task>
        {
            _db.SortedSetAddAsync(sortedSetKey, timestamp, member),
            _db.StringSetAsync(
                $"driver:current:{ping.DriverId}",
                $"{ping.Latitude},{ping.Longitude},{ping.Speed},{ping.Bearing},{geohash7}",
                TimeSpan.FromSeconds(StaleDriverTTLSeconds)),
            _kafkaProducer.ProduceAsync("location-events", ping.DriverId, new LocationEvent
            {
                DriverId = ping.DriverId,
                Latitude = ping.Latitude,
                Longitude = ping.Longitude,
                Speed = ping.Speed,
                Bearing = ping.Bearing,
                Geohash = geohash7,
                Timestamp = timestamp
            })
        };

        await Task.WhenAll(tasks);
    }

    private bool IsValidCoordinate(double lat, double lng)
    {
        return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
    }
}

public static class Geohash
{
    private const string Base32 = "0123456789bcdefghjkmnpqrstuvwxyz";

    public static string Encode(double latitude, double longitude, int precision)
    {
        double minLat = -90, maxLat = 90;
        double minLng = -180, maxLng = 180;
        var hash = new char[precision];
        int bit = 0, ch = 0;
        bool isLon = true;

        for (int i = 0; i < precision;)
        {
            if (isLon)
            {
                double mid = (minLng + maxLng) / 2;
                if (longitude >= mid) { ch |= (1 << (4 - bit)); minLng = mid; }
                else { maxLng = mid; }
            }
            else
            {
                double mid = (minLat + maxLat) / 2;
                if (latitude >= mid) { ch |= (1 << (4 - bit)); minLat = mid; }
                else { maxLat = mid; }
            }
            isLon = !isLon;
            if (bit < 4) bit++;
            else { hash[i++] = Base32[ch]; bit = 0; ch = 0; }
        }
        return new string(hash);
    }
}

6. Proximity Search - Finding Nearby Drivers in Real-Time

Finding the nearest available drivers to a rider's pickup location is the most latency-critical query in the entire system. The matching service must return a ranked list of nearby drivers within 200ms. This is a spatial proximity search over a dynamic dataset that changes every 3-4 seconds as drivers move. The solution uses geohash-based grid partitioning to convert the continuous spatial problem into a discrete lookup problem that Redis can answer efficiently.

Expanding Ring Search Algorithm

The expanding ring search starts at the finest geohash precision (level 7, 153m cells) and progressively expands to coarser precisions until enough driver candidates are found. This approach minimizes the number of drivers scanned while ensuring we find candidates even in areas with low driver density.

C#
public class ProximitySearchService
{
    private readonly IConnectionMultiplexer _redis;
    private const int MinCandidates = 3;
    private const int MaxSearchRadius = 5000;

    public async Task<List<DriverCandidate>> FindNearbyDriversAsync(
        double riderLat, double riderLng)
    {
        var candidates = new List<DriverCandidate>();
        var seenDrivers = new HashSet<string>();

        for (int precision = 7; precision >= 5 && candidates.Count < MinCandidates; precision--)
        {
            string centerHash = Geohash.Encode(riderLat, riderLng, precision);
            string[] neighbors = Geohash.GetNeighbors(riderLat, riderLng, precision);
            string[] allCells = new[] { centerHash }.Concat(neighbors).ToArray();

            foreach (string cell in allCells)
            {
                string redisKey = $"drivers:active:{cell}";
                var db = _redis.GetDatabase();
                long tenSecondsAgo = DateTimeOffset.UtcNow
                    .AddSeconds(-10).ToUnixTimeMilliseconds();

                var members = await db.SortedSetRangeByScoreAsync(
                    redisKey, tenSecondsAgo, double.PositiveInfinity);

                foreach (var entry in members)
                {
                    string member = entry.ToString();
                    string driverId = member.Split(':')[0];
                    if (seenDrivers.Contains(driverId)) continue;
                    seenDrivers.Add(driverId);

                    bool isAvailable = await IsDriverAvailableAsync(driverId);
                    if (!isAvailable) continue;

                    var loc = await GetCurrentLocationAsync(driverId);
                    if (loc == null) continue;

                    double distance = CalculateDistance(
                        riderLat, riderLng, loc.Latitude, loc.Longitude);
                    if (distance > MaxSearchRadius) continue;

                    candidates.Add(new DriverCandidate
                    {
                        DriverId = driverId,
                        Distance = distance,
                        Latitude = loc.Latitude,
                        Longitude = loc.Longitude
                    });
                }
            }
        }
        return candidates;
    }

    private double CalculateDistance(double lat1, double lng1, double lat2, double lng2)
    {
        const double R = 6371000;
        double dLat = ToRadians(lat2 - lat1);
        double dLng = ToRadians(lng2 - lng1);
        double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
                   Math.Cos(ToRadians(lat1)) * Math.Cos(ToRadians(lat2)) *
                   Math.Sin(dLng / 2) * Math.Sin(dLng / 2);
        return R * 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
    }

    private double ToRadians(double degrees) => degrees * Math.PI / 180;
}

Comparison of Proximity Search Approaches

ApproachLookup TimeMemoryAccuracyComplexity
Geohash (Redis sorted sets)O(log N)Medium~99%Low
Quadtree (in-memory)O(log N)High~99%Medium
KD-Tree (in-memory)O(log N) avgHigh~99%Medium
Google S2 cellsO(log N)Medium~100%High
PostGIS spatial queryO(N) worstLow~100%Low
Linear scan (all drivers)O(N)None100%Trivial
Why Geohash wins at scale: The geohash approach with Redis sorted sets provides O(log N) lookup with constant-time writes. The expanding ring search averages 2-3 Redis queries before finding enough candidates. In dense cities like Manhattan, precision 7 returns 15-30 drivers per cell, so the search rarely expands beyond precision 7. The total time for proximity search is typically 10-30ms.

7. Matching Engine - From Ride Request to Driver Assignment

The matching engine is the most critical business logic component. When a rider requests a ride, the matching engine must: (1) validate the ride request, (2) compute the surge multiplier for the pickup geohash, (3) search for nearby available drivers, (4) score and rank candidates, (5) send ride requests to the top 3 drivers simultaneously, and (6) assign the trip to the first driver who accepts within a timeout window. This entire flow must complete within 200ms of the rider pressing the button.

Matching Algorithm

The driver scoring formula combines multiple weighted factors to select the optimal driver. Distance is the primary factor (weighted at 0.5) because riders want the fastest pickup. Driver rating is weighted at 0.2 to maintain quality. Acceptance rate is weighted at 0.15 to reward drivers who reliably accept rides. Trip completion rate is weighted at 0.15 to prefer experienced drivers.

C#
public class MatchingService
{
    private readonly IProximitySearchService _proximitySearch;
    private readonly IPricingService _pricingService;
    private readonly IKafkaProducer _kafkaProducer;
    private readonly TimeSpan MatchTimeout = TimeSpan.FromSeconds(15);

    public async Task<Trip> MatchRiderToDriverAsync(RideRequest request)
    {
        ValidateRideRequest(request);

        string pickupGeohash = Geohash.Encode(
            request.PickupLat, request.PickupLng, 6);
        decimal surgeMultiplier = await _pricingService
            .GetSurgeMultiplierAsync(pickupGeohash);
        decimal fareEstimate = await _pricingService
            .EstimateFareAsync(request, surgeMultiplier);

        var candidates = await _proximitySearch.FindNearbyDriversAsync(
            request.PickupLat, request.PickupLng);

        if (!candidates.Any())
            throw new NoDriversAvailableException(
                "No drivers available nearby. Please try again later.");

        var scoredCandidates = candidates
            .Select(c => new
            {
                Driver = c,
                Score = CalculateMatchScore(request, c)
            })
            .OrderBy(x => x.Score)
            .Take(3)
            .ToList();

        var trip = await CreatePendingTripAsync(request, fareEstimate, surgeMultiplier);

        var acceptedDriver = await SendMatchRequestsToDriversAsync(
            scoredCandidates.Select(x => x.Driver).ToList(),
            trip, MatchTimeout);

        if (acceptedDriver == null)
            throw new MatchTimeoutException("No driver accepted within timeout");

        trip.DriverId = acceptedDriver.DriverId;
        trip.Status = TripStatus.Accepted;
        await UpdateTripAsync(trip);
        return trip;
    }

    private double CalculateMatchScore(RideRequest request, DriverCandidate driver)
    {
        double distanceScore = Math.Min(driver.Distance / 5000.0, 1.0);
        double ratingScore = 1.0 - (driver.Rating - 1.0) / 4.0;
        double acceptanceScore = 1.0 - (driver.AcceptanceRate / 100.0);
        double completionScore = 1.0 - (driver.CompletionRate / 100.0);

        return (distanceScore * 0.50) + (ratingScore * 0.20) +
               (acceptanceScore * 0.15) + (completionScore * 0.15);
    }
}
sequenceDiagram participant Rider participant API GW participant Trip Service participant Pricing Service participant Matching Service participant Proximity Search participant Redis participant Driver1 participant Driver2 participant Driver3 Rider->>API GW: Request ride (pickup, dropoff) API GW->>Trip Service: Create ride request Trip Service->>Pricing Service: Get fare estimate + surge Pricing Service-->>Trip Service: Fare: $25.50, Surge: 1.3x Trip Service->>Matching Service: Find best driver Matching Service->>Proximity Search: Search nearby drivers Proximity Search->>Redis: Query geohash cells Redis-->>Proximity Search: 5 candidates found Proximity Search-->>Matching Service: Top 3 scored candidates par Send requests to 3 drivers simultaneously Matching Service->>Driver1: Ride request (accept/reject) Matching Service->>Driver2: Ride request (accept/reject) Matching Service->>Driver3: Ride request (accept/reject) end Driver2-->>Matching Service: Accept (after 3 seconds) Matching Service->>Trip Service: Trip assigned to Driver2 Trip Service->>Rider: Driver found! ETA: 4 min Trip Service->>Driver2: Navigate to pickup
Common Pitfall: Never send ride requests sequentially to drivers. Sending to one driver, waiting for timeout, then trying the next driver adds 15+ seconds of latency per attempt. Always send to all top candidates simultaneously and take the first acceptance.

8. Surge Pricing - Dynamic Fare Calculation

Surge pricing is Uber's most controversial and most important algorithmic feature. At its core, surge pricing is a supply-demand balancing mechanism: when demand for rides exceeds the supply of available drivers in a geographic area, prices increase to both reduce demand and incentivize more drivers to come online. The pricing service continuously monitors the supply-demand ratio per geohash cell and computes a multiplier that is displayed to riders in real-time.

Surge Pricing Algorithm

The surge multiplier is calculated per geohash cell at precision level 6 (~1.2km). The algorithm considers three metrics over a 5-minute sliding window: (1) the ride request rate, (2) the available driver count, and (3) the driver acceptance rate. The multiplier updates every 30 seconds with smooth transitions to prevent price jarring.

C#
public class SurgePricingService : ISurgePricingService
{
    private readonly IConnectionMultiplexer _redis;
    private const int SlidingWindowSeconds = 300;
    private const int UpdateIntervalSeconds = 30;

    private static readonly (double RatioThreshold, decimal Multiplier)[] SurgeTable =
    {
        (1.0, 1.0m),
        (1.5, 1.2m),
        (2.0, 1.5m),
        (3.0, 2.0m),
        (5.0, 3.0m),
        (8.0, 5.0m),
    };

    public async Task<decimal> CalculateSurgeMultiplierAsync(string geohash6)
    {
        var db = _redis.GetDatabase();
        long windowStart = DateTimeOffset.UtcNow
            .AddSeconds(-SlidingWindowSeconds).ToUnixTimeMilliseconds();
        long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

        long requestCount = await db.SortedSetLengthAsync(
            $"requests:{geohash6}", windowStart, now);
        long driverCount = await db.SortedSetLengthAsync(
            $"drivers:active:{geohash6}", windowStart, now);
        long acceptedCount = await db.SortedSetLengthAsync(
            $"accepted:{geohash6}", windowStart, now);

        double requestRate = requestCount / (double)SlidingWindowSeconds;
        double driverAvailability = driverCount > 0
            ? (double)acceptedCount / Math.Max(1, requestCount) : 0;
        double effectiveSupply = driverCount * driverAvailability;
        double ratio = effectiveSupply > 0
            ? requestRate / (effectiveSupply / SlidingWindowSeconds)
            : double.MaxValue;

        decimal multiplier = 1.0m;
        foreach (var (threshold, mult) in SurgeTable)
        {
            if (ratio > threshold) multiplier = mult;
        }

        // Smooth transition: max 0.2x change per update
        string cacheKey = $"surge:current:{geohash6}";
        decimal previous = decimal.Parse(
            await db.StringGetAsync(cacheKey) ?? "1.0");
        decimal maxDelta = 0.2m;
        if (Math.Abs(multiplier - previous) > maxDelta)
        {
            multiplier = multiplier > previous
                ? previous + maxDelta : previous - maxDelta;
        }

        await db.StringSetAsync(cacheKey, multiplier.ToString(),
            TimeSpan.FromSeconds(UpdateIntervalSeconds * 2));
        return multiplier;
    }

    public async Task<FareEstimate> EstimateFareAsync(
        RideRequest request, decimal surgeMultiplier)
    {
        decimal baseFare = request.RideType switch
        {
            RideType.Economy => 2.50m,
            RideType.Comfort => 4.00m,
            RideType.Premium => 7.00m,
            _ => 2.50m
        };
        decimal perKmRate = request.RideType switch
        {
            RideType.Economy => 1.00m,
            RideType.Comfort => 1.50m,
            RideType.Premium => 2.50m,
            _ => 1.00m
        };
        decimal perMinuteRate = 0.20m;

        decimal distanceCost = request.EstimatedDistanceKm * perKmRate;
        decimal timeCost = request.EstimatedDurationMinutes * perMinuteRate;
        decimal subtotal = baseFare + distanceCost + timeCost;
        decimal totalWithSurge = subtotal * surgeMultiplier;
        decimal bookingFee = 1.50m;

        return new FareEstimate
        {
            BaseFare = baseFare,
            DistanceCost = distanceCost,
            TimeCost = timeCost,
            SurgeMultiplier = surgeMultiplier,
            BookingFee = bookingFee,
            EstimatedTotal = totalWithSurge + bookingFee,
            Currency = request.Currency
        };
    }
}

Surge Pricing Tiers

Demand/Supply RatioSurge MultiplierEstimated Fare ($25 base)Behavior
< 1.01.0x$25.00Normal pricing, slight discount possible
1.0 - 1.51.0x$25.00Balanced market, no surge
1.5 - 2.01.2x$30.00Mild surge, most riders accept
2.0 - 3.01.5x$37.50Moderate surge
3.0 - 5.02.0x$50.00High surge, strong demand signal
5.0 - 8.03.0x$75.00Very high surge, events or emergencies
> 8.05.0x$125.00Extreme surge, maximum price cap applies
Smooth Transitions: The surge multiplier smoothly transitions between levels by limiting the maximum change per update cycle to 0.2x. This prevents jarring price jumps for riders watching the surge indicator.

9. Trip Lifecycle and State Machine

Every ride follows a strictly defined lifecycle managed by the trip service as a state machine. The trip state machine ensures that all stakeholders have a consistent view of the trip's progress and that each transition triggers the appropriate side effects (notifications, payments, analytics).

Trip State Machine

stateDiagram-v2 [*] --> Requested: Rider presses button Requested --> Matching: Request accepted Matching --> DriverFound: Driver assigned DriverFound --> Accepted: Driver accepts Accepted --> DriverEnRoute: Driver heading to pickup DriverEnRoute --> Arrived: Driver at pickup Arrived --> InProgress: Trip starts InProgress --> Completed: Trip ends Completed --> Paid: Payment processed Paid --> Rated: Rating submitted Rated --> [*] Requested --> Cancelled: Rider cancels Matching --> Cancelled: Rider cancels Accepted --> Cancelled: Rider cancels DriverEnRoute --> Cancelled: Cancellation fee InProgress --> Emergency: SOS triggered Cancelled --> [*] Emergency --> [*]

Trip State Definitions

StateDescriptionValid TransitionsSide Effects
REQUESTEDRider submitted ride requestMATCHING, CANCELLEDBroadcast to matching service
MATCHINGSearching for driversDRIVER_FOUND, CANCELLEDNotify nearby drivers
DRIVER_FOUNDDriver assigned, awaiting acceptanceACCEPTED, CANCELLEDPush notification to driver
ACCEPTEDDriver confirmed the rideDRIVER_EN_ROUTE, CANCELLEDShare driver details with rider
DRIVER_EN_ROUTEDriver heading to pickupARRIVED, CANCELLEDReal-time tracking enabled
ARRIVEDDriver at pickup locationIN_PROGRESSNotify rider, start timer
IN_PROGRESSTrip in progressCOMPLETED, EMERGENCYLive tracking, route monitoring
COMPLETEDTrip ended at destinationPAIDCalculate final fare, process payment
PAIDPayment processedRATEDSend receipt, update earnings
CANCELLEDTrip cancelledTerminalCancel fee if applicable
C#
public class TripStateMachine
{
    private static readonly Dictionary<TripStatus, HashSet<TripStatus>> ValidTransitions = new()
    {
        [TripStatus.Requested] = new() { TripStatus.Matching, TripStatus.Cancelled },
        [TripStatus.Matching] = new() { TripStatus.DriverFound, TripStatus.Cancelled },
        [TripStatus.DriverFound] = new() { TripStatus.Accepted, TripStatus.Cancelled },
        [TripStatus.Accepted] = new() { TripStatus.DriverEnRoute, TripStatus.Cancelled },
        [TripStatus.DriverEnRoute] = new() { TripStatus.Arrived, TripStatus.Cancelled },
        [TripStatus.Arrived] = new() { TripStatus.InProgress },
        [TripStatus.InProgress] = new() { TripStatus.Completed, TripStatus.Emergency },
        [TripStatus.Completed] = new() { TripStatus.Paid },
        [TripStatus.Paid] = new() { TripStatus.Rated },
    };

    private readonly ITripRepository _tripRepo;
    private readonly IKafkaProducer _kafkaProducer;

    public async Task<Trip> TransitionAsync(string tripId, TripStatus newState, string triggeredBy)
    {
        var trip = await _tripRepo.GetByIdAsync(tripId)
            ?? throw new TripNotFoundException(tripId);

        if (!ValidTransitions.TryGetValue(trip.Status, out var allowed)
            || !allowed.Contains(newState))
        {
            throw new InvalidTransitionException(trip.Status, newState, tripId);
        }

        var previousStatus = trip.Status;
        trip.Status = newState;
        trip.UpdatedAt = DateTime.UtcNow;
        trip.StateHistory.Add(new TripStateTransition
        {
            From = previousStatus,
            To = newState,
            TriggeredBy = triggeredBy,
            Timestamp = DateTime.UtcNow
        });

        await _tripRepo.SaveAsync(trip);

        await _kafkaProducer.ProduceAsync("trip-state-events", tripId,
            new TripStateChangedEvent
        {
            TripId = tripId,
            PreviousState = previousStatus,
            NewState = newState,
            TriggeredBy = triggeredBy,
            Timestamp = DateTime.UtcNow
        });

        return trip;
    }
}

public enum TripStatus
{
    Requested, Matching, DriverFound, Accepted,
    DriverEnRoute, Arrived, InProgress,
    Completed, Paid, Rated, Cancelled, Emergency
}

10. Real-Time Trip Tracking with WebSockets

Real-time trip tracking is one of the most visible features of the ride-sharing experience. Once a driver is assigned to a trip, both the rider and driver need to see each other's location on a map in real-time. The tracking service maintains persistent WebSocket connections and uses a pub-sub pattern where the driver publishes location updates to a trip-specific topic and the rider subscribes to receive those updates.

WebSocket Architecture

The WebSocket gateway is a horizontally scalable cluster of servers that maintain persistent connections with client apps. Each server can handle approximately 100,000 concurrent WebSocket connections using async I/O. The gateway uses Redis Pub/Sub as the message bus for cross-server message routing: when a driver is connected to server A and the rider is connected to server B, the driver's location update is published to a Redis channel for the trip, and server B picks it up and delivers it to the rider.

C#
public class TripTrackingWebSocketHandler
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ISubscriber _subscriber;
    private readonly ConcurrentDictionary<string, WebSocket> _connections = new();

    public async Task HandleConnectionAsync(HttpContext context, string tripId, string userId)
    {
        using var ws = await context.WebSockets.AcceptAsyncAsync();
        string connectionId = Guid.NewGuid().ToString();
        _connections[connectionId] = ws;

        var channel = RedisChannel.Literal($"trip:location:{tripId}");
        await _subscriber.SubscribeAsync(channel, async (msg) =>
        {
            if (ws.State == WebSocketState.Open)
            {
                var payload = Encoding.UTF8.GetBytes(msg.Message);
                await ws.SendAsync(
                    new ArraySegment<byte>(payload),
                    WebSocketMessageType.Binary, true, CancellationToken.None);
            }
        });

        var buffer = new byte[1024];
        while (ws.State == WebSocketState.Open)
        {
            var result = await ws.ReceiveAsync(
                new ArraySegment<byte>(buffer), CancellationToken.None);

            if (result.MessageType == WebSocketMessageType.Binary)
            {
                var update = LocationUpdate.Parser.ParseFrom(
                    buffer.AsSpan(0, result.Count));

                var locationData = new TripLocationData
                {
                    Lat = update.Latitude,
                    Lng = update.Longitude,
                    Speed = update.Speed,
                    Bearing = update.Bearing,
                    Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
                };

                await _subscriber.PublishAsync(
                    RedisChannel.Literal($"trip:location:{tripId}"),
                    JsonSerializer.Serialize(locationData));
            }
        }
        _connections.TryRemove(connectionId, out _);
    }
}

Tracking Performance Metrics

MetricTargetMeasurement
Location update latency< 500ms end-to-endDriver GPS to Rider screen
WebSocket connections5M+ concurrentAcross all gateway servers
Messages per second125K+ location updatesAt peak load
Connection establishment< 200msTLS handshake + upgrade
Reconnection time< 2 secondsAfter network interruption
Protocol overhead< 50 bytes per messageProtocol Buffers vs 500 bytes JSON
Protocol Buffers: Uber uses Protocol Buffers for location updates over WebSocket instead of JSON. A protobuf-encoded location update is approximately 50 bytes, compared to 500 bytes for the equivalent JSON. At 125K messages per second, this saves approximately 56 MB/s of bandwidth.

11. Payment Processing and Fare Settlement

The payment system handles the most sensitive operations in the platform: charging riders, paying drivers, processing refunds, and managing split fares. Payment processing must be reliable, secure (PCI DSS compliant), and idempotent. The payment service follows a two-phase approach: pre-authorization when the trip starts and capture when the trip completes.

Payment Flow

sequenceDiagram participant Trip Service participant Payment Service participant Payment Gateway participant Rider participant Driver Note over Trip Service: Trip Completed Trip Service->>Payment Service: TripCompletedEvent (trip details, fare) Payment Service->>Payment Service: Calculate final fare from trip data Payment Service->>Payment Gateway: Capture pre-authorized amount Payment Gateway-->>Payment Service: Payment captured Payment Service->>Payment Service: Record payment transaction Payment Service-->>Trip Service: Payment successful par Notify both parties Trip Service->>Rider: Receipt with fare breakdown Trip Service->>Driver: Earnings credited end alt Payment Failed Payment Gateway-->>Payment Service: Payment declined Payment Service->>Payment Service: Add to retry queue Note over Payment Service: Retry up to 3 times, then escalate end

Fare Calculation Components

ComponentEconomyComfortPremiumCalculation
Base fare$2.50$4.00$7.00Fixed charge per trip
Per km$1.00$1.50$2.50Distance x rate
Per minute$0.20$0.30$0.50Duration x rate
Booking fee$1.50$2.00$2.50Fixed platform fee
Minimum fare$5.00$7.00$12.00Floor price
Cancellation (5 min+)$5.00$7.00$10.00After grace period
C#
public class PaymentService : IPaymentService
{
    private readonly IPaymentGateway _gateway;
    private readonly IPaymentRepository _repository;
    private readonly IKafkaProducer _kafkaProducer;

    public async Task<PaymentResult> ProcessTripPaymentAsync(Trip trip, FareBreakdown fare)
    {
        var existing = await _repository.GetByTripIdAsync(trip.TripId);
        if (existing != null && existing.Status == PaymentStatus.Captured)
            return PaymentResult.AlreadyProcessed(existing.PaymentId);

        var payment = existing ?? new Payment
        {
            PaymentId = Guid.NewGuid().ToString(),
            TripId = trip.TripId,
            RiderId = trip.RiderId,
            DriverId = trip.DriverId,
            Amount = fare.TotalAmount,
            Currency = fare.Currency,
            Status = PaymentStatus.Pending,
            CreatedAt = DateTime.UtcNow
        };

        try
        {
            var captureResult = await _gateway.CapturePaymentAsync(
                paymentId: payment.PaymentGatewayId,
                amount: fare.TotalAmount,
                idempotencyKey: $"capture-{trip.TripId}");

            if (captureResult.Success)
            {
                payment.Status = PaymentStatus.Captured;
                payment.PaymentGatewayTransactionId = captureResult.TransactionId;
                await _repository.SaveAsync(payment);

                decimal driverEarning = fare.TotalAmount * 0.75m;
                await _kafkaProducer.ProduceAsync("payment-events", trip.TripId,
                    new PaymentCompletedEvent
                {
                    PaymentId = payment.PaymentId,
                    TripId = trip.TripId,
                    TotalCharged = fare.TotalAmount,
                    DriverEarning = driverEarning,
                    PlatformFee = fare.TotalAmount - driverEarning
                });

                return PaymentResult.Success(payment.PaymentId);
            }
            else
            {
                payment.Status = PaymentStatus.Failed;
                payment.RetryCount++;
                await _repository.SaveAsync(payment);

                if (payment.RetryCount < 3)
                {
                    await _kafkaProducer.ProduceAsync("payment-retry", trip.TripId,
                        new PaymentRetryEvent
                    {
                        PaymentId = payment.PaymentId,
                        RetryAfter = TimeSpan.FromMinutes(5 * payment.RetryCount)
                    });
                }
                return PaymentResult.Failed(captureResult.ErrorMessage);
            }
        }
        catch (Exception ex)
        {
            payment.Status = PaymentStatus.Error;
            payment.ErrorMessage = ex.Message;
            await _repository.SaveAsync(payment);
            throw;
        }
    }
}

12. Data Storage Architecture and Sharding Strategy

Choosing the right storage technology for each data type is critical in a ride-sharing system. The data access patterns are dramatically different across services: driver locations require high-throughput writes with TTL-based expiration, trip data requires strong consistency with complex queries, and analytics require columnar storage for aggregation queries. The architecture uses polyglot persistence, the right tool for each job.

Storage Technology Selection

Data TypeDatabasePartition KeyReplicationRetention
Real-time driver locationsRedis ClusterGeohash6 prefix3 replicas per shard10 seconds (TTL)
Location historyCassandra(driver_id, date)RF=390 days
Trip recordsPostgreSQL (sharded)trip_id (hash)Primary + read replica5+ years
User profilesPostgreSQLuser_idPrimary + 2 read replicasIndefinite
Driver earningsPostgreSQLdriver_idPrimary + read replicaIndefinite
Payment transactionsPostgreSQLpayment_idPrimary + 2 read replicas7 years
Analytics eventsKafka to ClickHouseDate-based partitionRF=21 year

Trip Database Sharding

The trip database is the most heavily written relational data store. With 15 million trips per day, a single PostgreSQL instance cannot handle the write load. The solution is hash-based sharding: the trip_id is hashed (MD5), and the first 8 hex characters are converted to an integer that maps to one of 256 shards. Each shard runs on a dedicated PostgreSQL instance with a read replica for reporting queries.

C#
public class TripShardingService
{
    private const int ShardCount = 256;
    private readonly Dictionary<int, IDbConnection> _shardConnections;

    public TripShardingService(IConfiguration config)
    {
        _shardConnections = new Dictionary<int, IDbConnection>();
        for (int i = 0; i < ShardCount; i++)
        {
            string connString = config.GetConnectionString($"TripShard_{i}");
            _shardConnections[i] = new NpgsqlConnection(connString);
        }
    }

    public IDbConnection GetShard(string tripId)
    {
        using var md5 = MD5.Create();
        byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(tripId));
        int shardNum = Convert.ToInt32(
            BitConverter.ToString(hash, 0, 4).Replace("-", ""), 16) % ShardCount;
        return _shardConnections[shardNum];
    }

    public async Task<Trip> GetTripAsync(string tripId)
    {
        var conn = GetShard(tripId);
        return await conn.QueryFirstOrDefaultAsync<Trip>(
            "SELECT * FROM trips WHERE trip_id = @TripId",
            new { TripId = tripId });
    }
}

// Cassandra schema for location history
// CREATE TABLE location_history (
//     driver_id text,
//     date text,
//     timestamp timestamp,
//     lat double, lng double, speed double,
//     bearing double, geohash text,
//     PRIMARY KEY ((driver_id, date), timestamp)
// ) WITH CLUSTERING ORDER BY (timestamp DESC)
//   AND default_time_to_live = 7776000;

// PostgreSQL trip table
// CREATE TABLE trips (
//     trip_id VARCHAR(36) PRIMARY KEY,
//     rider_id VARCHAR(36) NOT NULL,
//     driver_id VARCHAR(36),
//     status VARCHAR(20) NOT NULL,
//     pickup_lat DOUBLE PRECISION,
//     pickup_lng DOUBLE PRECISION,
//     dropoff_lat DOUBLE PRECISION,
//     dropoff_lng DOUBLE PRECISION,
//     fare_estimate DECIMAL(10,2),
//     surge_multiplier DECIMAL(4,2) DEFAULT 1.0,
//     created_at TIMESTAMP DEFAULT NOW(),
//     completed_at TIMESTAMP,
//     distance_km DOUBLE PRECISION,
//     duration_minutes INTEGER
// );

Data Access Patterns by Service

ServiceWrite QPSRead QPSConsistencyAccess Pattern
Location Service125K50KEventualWrite: latest ping. Read: Redis sorted set by geohash
Trip Service5K20KStrongWrite: state transitions. Read: by trip_id, rider_id
Matching Service1005KEventualWrite: match result. Read: Redis driver locations
Pricing Service1K10KEventualWrite: surge values. Read: fare estimates by geohash
Payment Service2001KStrongWrite: charge/refund. Read: by payment_id, trip_id

13. Safety, Fraud Detection, and Anomaly Handling

Safety is paramount in a ride-sharing platform. The system must protect both riders and drivers through real-time monitoring, anomaly detection, and emergency response capabilities. The safety system operates as a separate pipeline that consumes trip and location events from Kafka and applies rule-based and ML-based detection models to identify suspicious patterns in real-time.

Safety Features and Implementation

FeatureImplementationLatencyPriority
SOS ButtonImmediate alert to safety team + local emergency services< 2 secCritical
Trip sharingRider shares live trip link with trusted contacts< 5 secHigh
Route deviationML model flags unusual route deviations during trip< 30 secHigh
Unusual stop detectionFlag stops longer than 2 minutes during active trip< 2 minMedium
Speed anomalyFlag excessive speed or sudden braking events< 10 secMedium
Identity verificationDriver selfie verification at shift start< 5 secHigh
PIN-based pickupRider shows PIN to driver to verify correct rideInstantMedium
C#
public class SafetyAnomalyDetector
{
    private readonly IKafkaConsumer _consumer;
    private readonly IAlertService _alertService;

    public async Task ProcessLocationAnomalyAsync(TripLocationEvent evt)
    {
        var trip = await _tripService.GetTripAsync(evt.TripId);
        var expectedRoute = await _routingService.GetRouteAsync(
            trip.PickupLat, trip.PickupLng,
            trip.DropoffLat, trip.DropoffLng);

        double deviationDistance = CalculateDeviationFromRoute(
            evt.Lat, evt.Lng, expectedRoute.Waypoints);

        if (deviationDistance > 500)
        {
            await _alertService.RaiseAlertAsync(new SafetyAlert
            {
                TripId = evt.TripId,
                AlertType = AlertType.RouteDeviation,
                Severity = Severity.Medium,
                Details = $"Driver deviated {deviationDistance}m from expected route",
                Timestamp = DateTime.UtcNow
            });
        }

        if (evt.Speed > 160)
        {
            await _alertService.RaiseAlertAsync(new SafetyAlert
            {
                TripId = evt.TripId,
                AlertType = AlertType.ExcessiveSpeed,
                Severity = Severity.High,
                Details = $"Speed detected: {evt.Speed} km/h",
                Timestamp = DateTime.UtcNow
            });
        }

        if (evt.Speed < 1 && trip.Status == TripStatus.InProgress)
        {
            var stopDuration = await TrackStopDurationAsync(evt.TripId, evt.DriverId);
            if (stopDuration > TimeSpan.FromMinutes(2))
            {
                await _alertService.RaiseAlertAsync(new SafetyAlert
                {
                    TripId = evt.TripId,
                    AlertType = AlertType.UnusualStop,
                    Severity = Severity.Medium,
                    Details = $"Stop duration: {stopDuration.TotalMinutes:F1} minutes"
                });
            }
        }
    }

    public async Task DetectFakeTripAsync(Trip trip)
    {
        if (trip.RiderId == trip.DriverId)
            await FlagForReviewAsync(trip.TripId, "Same rider and driver");

        double distance = CalculateDistance(
            trip.PickupLat, trip.PickupLng,
            trip.DropoffLat, trip.DropoffLng);
        if (distance < 100)
            await FlagForReviewAsync(trip.TripId, "Suspiciously short trip");

        int recentTrips = await _tripRepo.CountTripsBetweenAsync(
            trip.RiderId, trip.DriverId, TimeSpan.FromDays(7));
        if (recentTrips > 10)
            await FlagForReviewAsync(trip.TripId, "Repeated rider-driver pair");
    }
}

14. Caching, CDN, and Performance Optimization

Performance optimization in a ride-sharing system focuses on three areas: reducing read latency for high-QPS queries, minimizing write amplification in the location ingestion pipeline, and optimizing the real-time tracking WebSocket connections.

Caching Layers

LayerTechnologyWhat is CachedTTLHit Rate Target
L1 (In-Process)MemoryCachePricing config, city settings5 minutes> 99%
L2 (Distributed)Redis ClusterDriver locations, surge values, sessions10 sec - 24 hr> 95%
L3 (CDN)CloudFrontStatic assets, map tiles, app bundles1 hour - 7 days> 90%
L4 (Database)PostgreSQL read replicasTrip history, user profilesContinuous syncN/A

Key Optimizations

  • Connection pooling: The location service maintains a Redis connection pool with 500 connections per instance to handle 125K writes/sec without connection overhead.
  • Pipeline batching: Multiple Redis commands are pipelined into a single network round-trip. A batch of 100 location writes completes in ~2ms instead of ~200ms if sent individually.
  • Protocol Buffers: WebSocket location updates use protobuf instead of JSON, reducing payload size by 90% (50 bytes vs 500 bytes per ping).
  • Connection multiplexing: The WebSocket gateway multiplexes 10,000 client connections per server using async I/O.
  • Geo-distributed Redis: Redis clusters are deployed in each cloud region to eliminate cross-region latency for location queries.
  • Write-behind caching: Location writes go to Redis first (sub-ms), with async persistence to Cassandra via Kafka.
C#
public class BatchLocationWriter
{
    private readonly IConnectionMultiplexer _redis;
    private readonly int _batchSize = 100;
    private readonly ConcurrentQueue<LocationPing> _writeQueue = new();

    public async Task QueueLocationPingAsync(LocationPing ping)
    {
        _writeQueue.Enqueue(ping);
        if (_writeQueue.Count >= _batchSize)
            await FlushBatchAsync();
    }

    private async Task FlushBatchAsync()
    {
        var batch = new List<LocationPing>();
        while (batch.Count < _batchSize && _writeQueue.TryDequeue(out var ping))
            batch.Add(ping);

        if (!batch.Any()) return;

        var db = _redis.GetDatabase();
        var tasks = db.CreateBatch();

        foreach (var ping in batch)
        {
            string geohash6 = Geohash.Encode(ping.Latitude, ping.Longitude, 6);
            string geohash7 = Geohash.Encode(ping.Latitude, ping.Longitude, 7);
            string member = $"{ping.DriverId}:{geohash7}";
            long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

            tasks.SortedSetAddAsync($"drivers:active:{geohash6}", timestamp, member);
            tasks.StringSetAsync(
                $"driver:current:{ping.DriverId}",
                $"{ping.Latitude},{ping.Longitude},{ping.Speed}",
                TimeSpan.FromSeconds(10));
        }

        tasks.Execute();
        await Task.WhenAll(batch.Cast<Task>());
    }
}

15. Multi-Region Deployment and Disaster Recovery

Uber operates across multiple cloud regions worldwide, and the ride-sharing system must be resilient to datacenter failures, network partitions, and regional outages. The architecture uses active-active multi-region deployment where each region can independently serve ride requests for its geographic area.

graph TB subgraph US-East USE_Redis[(Redis Cluster)] USE_PG[(PostgreSQL Primary)] USE_Match[Matching Service] end subgraph US-West USW_Redis[(Redis Cluster)] USW_PG[(PostgreSQL Primary)] USW_Match[Matching Service] end subgraph EU-West EUW_Redis[(Redis Cluster)] EUW_PG[(PostgreSQL Primary)] EUW_Match[Matching Service] end subgraph Global Kafka_Global[Kafka MirrorMaker] DNS[Route 53 Geo DNS] end DNS --> USE_Match DNS --> USW_Match DNS --> EUW_Match Kafka_Global -.->|Async replication| USE_PG Kafka_Global -.->|Async replication| USW_PG Kafka_Global -.->|Async replication| EUW_PG

Regional Routing Table

RegionCitiesPeak QPSDriversRedis Shards
US-East (Virginia)New York, Boston, Miami, Chicago15,000800K24
US-West (Oregon)Los Angeles, San Francisco, Seattle12,000600K18
EU-West (Ireland)London, Paris, Berlin, Amsterdam18,0001M30
AP-South (Mumbai)Mumbai, Delhi, Bangalore20,0001.5M36
AP-East (Singapore)Singapore, Tokyo, Sydney10,000500K15

Disaster Recovery Procedures

When a region fails, the DNS layer (Route 53 with health checks) routes traffic to the nearest healthy region. The failover process is: (1) Health checks detect region failure within 30 seconds. (2) DNS TTL expires and traffic shifts to the next-closest region within 60 seconds. (3) The new region's matching service begins accepting ride requests. (4) Location data for drivers in the failed region is stale within 10 seconds (Redis TTL), but new pings will be ingested by whichever region they connect to. (5) Trip data is eventually consistent via Kafka MirrorMaker.

C#
public class RegionAwareServiceRegistry
{
    private readonly Dictionary<string, RegionConfig> _regions;

    public async Task<RegionConfig> GetOptimalRegionAsync(double lat, double lng)
    {
        var healthyRegions = _regions.Values
            .Where(r => r.IsHealthy)
            .OrderBy(r => CalculateDistance(lat, lng, r.CenterLat, r.CenterLng))
            .ToList();

        if (!healthyRegions.Any())
            throw new ServiceUnavailableException("All regions are down");

        var primary = healthyRegions.First();

        if (primary.Id != GetHomeRegion(lat, lng))
        {
            await Metrics.TrackAsync("region.failover.active", new
            {
                OriginalRegion = GetHomeRegion(lat, lng),
                ActualRegion = primary.Id
            });
        }

        return primary;
    }
}

public class RegionConfig
{
    public string Id { get; set; }
    public bool IsHealthy { get; set; }
    public double CenterLat { get; set; }
    public double CenterLng { get; set; }
    public string Endpoint { get; set; }
}

16. Monitoring, Observability, and SLA Management

Operating a ride-sharing platform at scale requires comprehensive monitoring and observability across all services. The three pillars of observability, metrics, logs, and traces, must cover every critical path in the system. The most critical metrics are matching success rate, location ping freshness, trip completion rate, and payment success rate.

Critical SLAs and Metrics

MetricSLA TargetMeasurementAlert Threshold
Matching success rate> 98%Trips matched / ride requests< 95% over 5 min
Matching latency (p99)< 200msRequest to driver assignment> 500ms over 2 min
Location ping freshness> 99% within 5 sec% of driver pings < 5 sec old< 95% fresh
Trip completion rate> 99.5%Completed trips / started trips< 99% over 15 min
Payment success rate> 99.9%Successful charges / attempted< 99% over 10 min
API availability99.99%Successful requests / total< 99.9% over 5 min
WebSocket connection rate> 99.9%Connected drivers / total< 99% over 3 min
Surge pricing freshness< 30 sec staleMax age of surge multiplier> 60 sec stale

Alerting Strategy

The alerting hierarchy follows a severity-based approach. P0 (Critical) alerts page on-call engineers immediately and include: matching service down, payment service down, datacenter failure. P1 (High) alerts send Slack notifications and include: matching latency above SLA, location write failures exceeding 1%. P2 (Medium) alerts are reviewed during business hours and include: surge pricing anomalies, elevated cancellation rates. P3 (Low) alerts are tracked in dashboards and include: cache hit rate degradation, slow database queries.

C#
public class RideSharingMetricsService
{
    private readonly IMetricsCollector _metrics;

    public void RecordMatchingMetrics(MatchResult result)
    {
        _metrics.Histogram("matching.latency_ms", result.LatencyMs);
        _metrics.Counter("matching.request.total", 1);
        _metrics.Counter($"matching.result.{result.Status.ToString().ToLower()}", 1);

        if (result.LatencyMs > 200)
            _metrics.Counter("matching.slow_request", 1);

        if (result.Status == MatchStatus.NoDriversFound)
            _metrics.Counter("matching.no_drivers", 1);
    }

    public void RecordLocationMetrics(LocationPing ping, bool isStale)
    {
        _metrics.Histogram("location.ingest_latency_ms", ping.IngestLatencyMs);
        _metrics.Counter("location.ping.total", 1);
        if (isStale) _metrics.Counter("location.stale_ping", 1);
        _metrics.Gauge("location.active_drivers", GetActiveDriverCount());
    }

    public void RecordTripMetrics(Trip trip)
    {
        _metrics.Counter("trip.created", 1);
        _metrics.Histogram("trip.duration_minutes", trip.DurationMinutes);
        _metrics.Histogram("trip.distance_km", trip.DistanceKm);
        _metrics.Histogram("trip.fare_amount", trip.FareAmount);

        if (trip.Status == TripStatus.Completed)
            _metrics.Counter("trip.completed", 1);
        else if (trip.Status == TripStatus.Cancelled)
            _metrics.Counter("trip.cancelled", 1);
    }
}

17. Cost Estimation and Infrastructure Sizing

Running a ride-sharing platform at global scale requires significant infrastructure investment. The cost model must account for compute, storage, network, and third-party services. Understanding these costs is essential for business viability and for making informed engineering trade-offs during system design.

Monthly Infrastructure Cost Estimate

ComponentSpecCountMonthly Cost
Application serversc5.2xlarge (8 vCPU, 16GB)200$62,000
WebSocket gateway serversc5.4xlarge (16 vCPU, 32GB)100$48,000
Redis clusterr6g.2xlarge (8 vCPU, 52GB)150$85,000
PostgreSQL (256 shards + replicas)r5.2xlarge (8 vCPU, 64GB)512$245,000
Cassandrai3.2xlarge (8 vCPU, 61GB SSD)100$78,000
Kafka clusterm5.2xlarge (8 vCPU, 32GB)30$18,000
Data transfer~50 TB/monthN/A$15,000
Mapping API100M API calls/monthN/A$50,000
Payment gateway fees2.9% + $0.30 per txn450M/month$2,500,000
Push notifications500M messages/monthN/A$5,000
MonitoringDatadog / Grafana CloudN/A$25,000
Total estimated monthly~$3,131,000
Cost Optimization: The largest cost is payment processing fees (80% of total infrastructure cost). Key optimizations include: reserved instances for 40% compute savings, cold data archival for trip history older than 1 year, and Kafka compression to reduce data transfer costs by 60%.

18. Complete C# Implementation - Matching and Trip Services

This section provides production-quality C# implementations for the two most critical services: the matching engine and the trip lifecycle manager. These implementations demonstrate proper use of dependency injection, async/await patterns, error handling, and integration with Redis and Kafka.

Matching Service Controller

C#
public class MatchingGrpcService : MatchingService.MatchingServiceBase
{
    private readonly IMatchingEngine _engine;
    private readonly ILogger<MatchingGrpcService> _logger;

    public MatchingGrpcService(IMatchingEngine engine, ILogger<MatchingGrpcService> logger)
    {
        _engine = engine;
        _logger = logger;
    }

    public override async Task<MatchResponse> RequestRide(
        RideRequest request, ServerCallContext context)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            _logger.LogInformation(
                "Ride request from {RiderId} at ({Lat}, {Lng})",
                request.RiderId, request.PickupLatitude, request.PickupLongitude);

            ValidateRequest(request);

            var result = await _engine.MatchAsync(new MatchRequest
            {
                RiderId = request.RiderId,
                PickupLat = request.PickupLatitude,
                PickupLng = request.PickupLongitude,
                DropoffLat = request.DropoffLatitude,
                DropoffLng = request.DropoffLongitude,
                RideType = Enum.Parse<RideType>(request.RideType)
            });

            sw.Stop();
            _logger.LogInformation("Match completed in {Elapsed}ms", sw.ElapsedMilliseconds);

            return new MatchResponse
            {
                Success = true,
                TripId = result.TripId,
                DriverId = result.DriverId,
                ETA = result.PickupETA,
                FareEstimate = result.FareEstimate,
                SurgeMultiplier = result.SurgeMultiplier
            };
        }
        catch (NoDriversAvailableException ex)
        {
            return new MatchResponse
            {
                Success = false,
                ErrorCode = "NO_DRIVERS",
                ErrorMessage = "No drivers available nearby"
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Matching failed for rider {RiderId}", request.RiderId);
            return new MatchResponse
            {
                Success = false,
                ErrorCode = "INTERNAL_ERROR",
                ErrorMessage = "Service temporarily unavailable"
            };
        }
    }
}

public class MatchingEngine : IMatchingEngine
{
    private readonly IProximitySearchService _proximity;
    private readonly IPricingService _pricing;
    private readonly ITripService _trips;
    private readonly ICircuitBreaker _breaker;

    public async Task<MatchResult> MatchAsync(MatchRequest request)
    {
        return await _breaker.ExecuteAsync(async () =>
        {
            string geohash6 = Geohash.Encode(request.PickupLat, request.PickupLng, 6);
            decimal surge = await _pricing.GetSurgeMultiplierAsync(geohash6);

            var candidates = await _proximity.FindNearbyDriversAsync(
                request.PickupLat, request.PickupLng);

            if (!candidates.Any())
                throw new NoDriversAvailableException();

            var ranked = candidates
                .Select(c => new { c, Score = ScoreDriver(request, c) })
                .OrderBy(x => x.Score)
                .Take(3)
                .Select(x => x.c)
                .ToList();

            var trip = await _trips.CreatePendingTripAsync(request, surge);
            var accepted = await FanOutRequestsAsync(ranked, trip);

            if (accepted == null)
                throw new MatchTimeoutException("No driver accepted within 15s");

            return await _trips.ConfirmTripAsync(trip.TripId, accepted.DriverId);
        },
        fallback: () => FallbackMatchAsync(request),
        timeout: TimeSpan.FromSeconds(20));
    }

    private double ScoreDriver(MatchRequest request, DriverCandidate driver)
    {
        double distance = HaversineDistance(
            request.PickupLat, request.PickupLng,
            driver.Latitude, driver.Longitude);
        double distNorm = Math.Min(distance / 5000.0, 1.0);
        double ratingNorm = 1.0 - (driver.Rating - 1.0) / 4.0;
        double acceptNorm = 1.0 - driver.AcceptanceRate / 100.0;

        return distNorm * 0.5 + ratingNorm * 0.2 + acceptNorm * 0.15 +
               (1.0 - driver.CompletionRate / 100.0) * 0.15;
    }
}

Trip Lifecycle Manager

C#
public class TripLifecycleService
{
    private readonly ITripRepository _repo;
    private readonly TripStateMachine _stateMachine;
    private readonly IKafkaProducer _kafka;
    private readonly IRoutingService _routing;
    private readonly ILogger<TripLifecycleService> _logger;

    public async Task<Trip> StartTripAsync(string tripId)
    {
        var trip = await _repo.GetByIdAsync(tripId);
        trip = await _stateMachine.TransitionAsync(
            tripId, TripStatus.InProgress, "driver");

        _ = MonitorTripRouteAsync(trip);

        await _kafka.ProduceAsync("trip-events", tripId, new TripStartedEvent
        {
            TripId = tripId,
            DriverId = trip.DriverId,
            RiderId = trip.RiderId,
            StartedAt = DateTime.UtcNow
        });

        _logger.LogInformation("Trip {TripId} started", tripId);
        return trip;
    }

    public async Task<Trip> CompleteTripAsync(string tripId)
    {
        var trip = await _repo.GetByIdAsync(tripId);
        var fare = await CalculateFinalFareAsync(trip);

        trip = await _stateMachine.TransitionAsync(
            tripId, TripStatus.Completed, "driver");
        trip.FinalFare = fare.TotalAmount;
        trip.DistanceKm = fare.DistanceKm;
        trip.DurationMinutes = fare.DurationMinutes;
        await _repo.SaveAsync(trip);

        await _kafka.ProduceAsync("trip-events", tripId, new TripCompletedEvent
        {
            TripId = tripId,
            RiderId = trip.RiderId,
            DriverId = trip.DriverId,
            FinalFare = fare.TotalAmount,
            DistanceKm = fare.DistanceKm,
            DurationMinutes = fare.DurationMinutes,
            SurgeMultiplier = fare.SurgeMultiplier
        });

        return trip;
    }

    private async Task<FareBreakdown> CalculateFinalFareAsync(Trip trip)
    {
        double distance = await _routing.CalculateTripDistanceAsync(
            trip.PickupLat, trip.PickupLng,
            trip.DropoffLat, trip.DropoffLng);
        int duration = (int)(DateTime.UtcNow - trip.StartedAt!.Value).TotalMinutes;

        decimal perKm = trip.RideType == RideType.Premium ? 2.50m : 1.00m;
        decimal baseFare = 2.50m;
        decimal distanceCost = (decimal)distance * perKm;
        decimal timeCost = duration * 0.20m;
        decimal subtotal = baseFare + distanceCost + timeCost;
        decimal total = subtotal * trip.SurgeMultiplier;

        return new FareBreakdown
        {
            BaseFare = baseFare,
            DistanceCost = distanceCost,
            TimeCost = timeCost,
            SurgeMultiplier = trip.SurgeMultiplier,
            TotalAmount = Math.Max(total, 5.00m),
            DistanceKm = distance,
            DurationMinutes = duration
        };
    }
}

Service Communication Matrix

FromToProtocolPatternSLA
Rider AppAPI GatewayHTTPS/RESTSynchronous< 100ms
Driver AppWebSocket GWWSSLong-lived connection< 500ms
API GatewayTrip ServicegRPCSynchronous< 100ms
Trip ServiceMatching ServicegRPCSynchronous< 200ms
Matching ServiceLocation ServicegRPCSynchronous< 50ms
Location ServiceRedisRedis protocolSynchronous< 5ms
Location ServiceKafkaKafka protocolAsync fire-and-forget< 10ms
KafkaNotification ServiceKafka consumerAsync pull< 5s
Payment ServiceStripe/BraintreeHTTPS/RESTSynchronous< 500ms

19. Interview Q&A - 25 Questions with Detailed Answers

Architecture Questions

Q1: How does Uber find nearby drivers quickly at scale?

Answer: Uber uses geohash encoding to partition the world into grid cells. Drivers send GPS pings every 3-4 seconds, which are stored in Redis sorted sets indexed by geohash prefix at precision level 6 (~1.2km cells). When a rider requests a ride, the matching service queries the rider's geohash cell and its 8 neighbors at precision 7 (~153m). If insufficient drivers are found, the search expands to coarser precisions using an expanding ring algorithm. Redis sorted sets provide O(log N) lookup, and the entire proximity search completes in 10-30ms.

Q2: How does surge pricing work technically?

Answer: The pricing service computes a surge multiplier per geohash cell at level 6 by monitoring the supply-demand ratio over a 5-minute sliding window. It counts ride requests, available drivers, and accepted trips. The ratio is mapped to a surge multiplier through a piecewise function (1.0x below 1.5 ratio, scaling to 5.0x above 8.0 ratio). The multiplier updates every 30 seconds with smooth transitions (max 0.2x change per update) to prevent price jarring.

Q3: What happens when a datacenter goes down mid-trip?

Answer: Active-active multi-region deployment ensures continuity. DNS health checks detect the failure within 30 seconds and redirect traffic to the nearest healthy region. In-progress trips are recovered from Kafka (which mirrors across regions) within 5 minutes. Drivers reconnect to the new region's WebSocket gateway. Trip state is durable in PostgreSQL, so no trip data is lost.

Q4: How do you handle the thundering herd during peak events?

Answer: The system handles spikes through: (1) Auto-scaling the matching service to 5x capacity using pre-provisioned instances. (2) Rate limiting ride requests per rider. (3) Showing higher surge multipliers to reduce demand. (4) Pre-computing surge values every 10 seconds instead of 30. (5) Priority queues for premium ride requests. (6) Kafka buffers the event spike, allowing consumers to process at their own pace.

Q5: How would you design the ETA calculation service?

Answer: ETA calculation uses: (1) Real-time traffic data from Google Maps API or a custom routing service. (2) Historical travel time data for the same route at the same time of day. (3) Current driver speed from the latest GPS ping. (4) Road network graph for pathfinding. The ETA for a driver en route to pickup is distance / average speed along the optimal route, while trip ETA uses multi-segment estimates that account for traffic on each road segment.

Scale and Performance Questions

Q6: How many Redis instances do you need for 500K concurrent drivers?

Answer: With 500K drivers, the total Redis memory is approximately 100 MB for active driver data. However, a Redis cluster with 6-12 shards (each with a replica) provides sufficient memory and throughput. The key bottleneck is write throughput: 125K writes/sec across the cluster, which each shard handles at ~10K writes/sec.

Q7: How do you ensure location data consistency between Redis and Cassandra?

Answer: Location data follows eventual consistency. Redis is the source of truth for real-time queries, and Cassandra is the source of truth for historical data. The ingestion service writes to Redis synchronously and publishes to Kafka for async Cassandra persistence. If Redis loses data, the recovery process replays the last 30 seconds of Kafka events to repopulate Redis.

Q8: What is the read-to-write ratio for each service?

Answer: Location Service: 40% writes (125K/sec), 60% reads (proximity queries, 50K/sec). Trip Service: 20% writes (5K/sec), 80% reads (20K/sec). Matching Service: 5% writes (match results), 95% reads (driver lookups, 5K/sec). Pricing Service: 10% writes (surge updates), 90% reads (fare estimates, 10K/sec). Payment Service: 50% writes (charges), 50% reads (receipt lookups).

Q9: How would you handle a spike in ride requests for a concert ending?

Answer: A concert creates a localized demand spike. The system responds by: (1) Surge pricing kicks in immediately, tripling or more to balance demand. (2) Pre-scheduled driver incentives notify nearby drivers 30 minutes before the concert ends. (3) Matching service auto-scales to handle 10x QPS. (4) The system shows increased ETAs to riders, encouraging some to wait or use alternatives. (5) Pooled rides are promoted to increase driver utilization.

Q10: How do you prevent race conditions in the matching engine?

Answer: When multiple riders request rides simultaneously in the same area, the same driver could be matched to multiple riders. The solution uses Redis-based distributed locks with TTL: when a driver receives a match request, a lock is acquired on the driver ID. If the driver accepts, the lock is converted to a trip assignment. If the driver rejects or the lock expires (15 seconds), the driver becomes available again. This ensures each driver handles only one match request at a time.

Data Modeling Questions

Q11: How do you model the trip data in PostgreSQL?

Answer: The trips table uses trip_id (UUID) as the primary key, with composite indexes on (rider_id, created_at DESC) and (driver_id, created_at DESC) for the two primary read patterns. The table includes pickup/dropoff coordinates as DOUBLE PRECISION, fare fields as DECIMAL(10,2), and status as VARCHAR(20) with a partial index for active trips. Trip state history is stored in a separate trips_state_history table linked by trip_id for audit purposes.

Q12: Why not use PostGIS instead of Redis for proximity search?

Answer: PostGIS supports spatial queries using GiST indexes, but it cannot match Redis performance for the required throughput. PostGIS spatial queries take 5-50ms, while Redis sorted set lookups take 1-5ms. At 125K writes/sec for location updates, PostgreSQL would be overwhelmed. Redis handles the write load and provides sub-millisecond reads. The design uses PostGIS for batch analytics queries (finding drivers in a region for reporting) but not for real-time matching.

Q13: How do you handle trip data for completed trips older than 5 years?

Answer: Trip data older than 1 year is moved to a cold storage tier. The archival process runs nightly: trips with created_at older than 365 days are exported to Parquet files in S3 and removed from the primary PostgreSQL shards. The Parquet files are queryable via AWS Athena for compliance and analytics. Trip receipts and payment records are retained indefinitely in compressed form. This reduces PostgreSQL storage by approximately 70%.

Design Trade-off Questions

Q14: How do you balance matching quality vs. latency?

Answer: The 200ms latency budget limits the matching algorithm complexity. The expanding ring search with geohash Redis sets provides fast candidate retrieval (10-30ms). The scoring algorithm is a simple weighted sum (O(N log N) where N is typically 5-20 candidates), taking less than 1ms. The bottleneck is the 15-second timeout for driver acceptance. To improve quality without adding latency, pre-computed driver quality scores are maintained in Redis and updated hourly.

Q15: How do you handle consistency for trip payments vs. location updates?

Answer: Trip payments require strong consistency (exactly-once processing) because financial accuracy is non-negotiable. The payment service uses idempotency keys and database transactions to prevent double charges. Location updates are eventually consistent because a few seconds of stale data does not impact ride safety. The Kafka consumer for payments uses manual offset commits to ensure at-least-once delivery with idempotent processing.

Q16: What is the trade-off between geohash precision levels?

Answer: Higher precision (level 7-8) gives more accurate proximity but requires scanning more cells in sparse areas. Lower precision (level 4-5) covers larger areas but returns too many candidates in dense areas. The expanding ring approach gives the best of both worlds: start with high precision for fast results in dense areas, expand to lower precision only when needed. In Manhattan, 95% of searches complete at precision 7. In rural areas, the search typically reaches precision 5.

Q17: Why use WebSocket instead of HTTP long-polling for driver location?

Answer: HTTP long-polling requires a new TCP connection (or HTTP/2 stream) for each location update, adding 50-100ms overhead per ping. With 500K drivers sending pings every 4 seconds, long-polling would require 125K new connections per second. WebSocket maintains a single persistent connection per driver, reducing connection establishment overhead to zero after the initial handshake. WebSocket also enables bidirectional communication: the server can push ride requests to drivers without the driver polling.

Advanced Questions

Q18: How would you implement pooled rides (UberPool/Line)?

Answer: Pooled rides require matching multiple riders going in the same direction to a single driver. The matching algorithm extends the scoring function to consider route overlap: a driver carrying one rider is eligible for a second rider if the detour is less than 10 minutes. The system uses a route similarity score based on the shared path percentage. The trip data model adds a riders[] array to support multiple passengers. Fare splitting is computed as: each rider pays their proportion of the total distance minus the shared portion discount.

Q19: How do you handle cross-city or airport trips?

Answer: Airport trips have special handling: dedicated airport queues where drivers wait for passengers, fixed airport pickup zones, and different pricing tiers. The system maintains a separate geohash index for airport zones with pre-computed surge values. Airport trips often have longer wait times, so the matching service extends the search radius to 10km (vs 5km for normal trips). Cross-city trips may cross regional boundaries, so the trip is assigned to the pickup region for billing purposes.

Q20: How do you test the matching engine in production safely?

Answer: A/B testing is critical for matching algorithm changes. The system uses feature flags to route a percentage of ride requests to the new algorithm. Metrics are tracked separately: match rate, average ETA, rider satisfaction score, and driver utilization rate. A typical A/B test runs for 2 weeks with 5% traffic. If the new algorithm shows improvement, traffic is gradually ramped to 100%. Shadow mode testing runs the new algorithm alongside the old one without affecting real rides.

Behavioral and System Design Questions

Q21: How would you design the rating system after a trip?

Answer: After trip completion, both rider and driver see a rating screen. Riders rate drivers 1-5 stars and can leave text feedback. Drivers rate riders 1-5 stars. Ratings are stored in a separate ratings table linked to trip_id. The average rating is maintained as a cached value in the user profile, updated atomically using a write-through pattern. A driver with a rating below 4.3 is flagged for review. The system prevents self-ratings by requiring both parties to submit before either rating is revealed.

Q22: How do you handle driver going offline during a ride request?

Answer: If a driver goes offline after receiving a match request but before accepting, the system detects the disconnect via WebSocket close event. The pending match request is cancelled after a 5-second timeout, and the driver is removed from the availability index. The matching service immediately retries with the next set of candidates. This scenario occurs approximately 2% of the time and adds 3-5 seconds to the average matching latency.

Q23: How would you scale this system to support autonomous vehicles?

Answer: Autonomous vehicles (AVs) change several assumptions: they can be dispatched remotely (no driver acceptance needed), they have higher-frequency location updates (10Hz vs 0.25Hz), and they require much tighter ETA accuracy. The matching algorithm simplifies to pure distance optimization (no rating/acceptance factors). The location pipeline bandwidth increases 40x, requiring edge computing near AV fleets. The trip state machine adds remote start, remote stop, and emergency stop states.

Q24: How do you ensure data privacy for rider and driver location data?

Answer: Location data is encrypted at rest (AES-256) and in transit (TLS 1.3). Driver location history is retained for only 90 days by default. Rider pickup/dropoff locations are obfuscated in logs (rounded to 100m grid). Access to raw location data requires explicit authorization via a role-based access control system. GDPR/CCPA requests for data deletion trigger async cleanup of all location and trip data within 30 days. Anonymized location data is used for analytics after 90 days.

Q25: What would you change if redesigning this system from scratch today?

Answer: Three key changes: (1) Replace Redis sorted sets with a purpose-built geospatial index using Apache Geode or a custom in-memory quadtree for better memory efficiency at 5M+ drivers. (2) Use CRDTs (Conflict-free Replicated Data Types) for driver location state to enable conflict-free multi-region replication without coordination. (3) Adopt event sourcing for trip state management instead of the current mutable state model, enabling complete trip replay and simplified debugging. Additionally, I would build the entire platform on Kubernetes with service mesh (Istio/Linkerd) for automatic mTLS, traffic management, and observability.

20. Conclusion

Designing a ride-sharing system like Uber is a masterclass in distributed systems engineering. We have covered the entire architecture from GPS ping ingestion through geohash indexing, expanding ring proximity search, weighted matching algorithms, dynamic surge pricing with smooth transitions, state machine-driven trip lifecycle management, WebSocket-based real-time tracking, payment processing with idempotency, and multi-region disaster recovery. Each component presents unique challenges at scale, and the design choices must balance latency, consistency, availability, and cost.

The key takeaways for system design interviews are: (1) Always start with requirements and scale estimation, as they drive all subsequent design decisions. (2) Use the right data structure for the right job, geohash sorted sets in Redis for proximity, Cassandra for time-series, PostgreSQL for transactional data. (3) Design for graceful degradation, every component should have a fallback. (4) Separate the read and write paths, the location write path handles 125K writes/sec while the read path handles 50K proximity queries per second. (5) Use event-driven architecture for decoupling, Kafka is the backbone for all async workflows.

The numbers tell the story: 21.6 billion location pings per day, 15 million trips, 40,000 writes per second at peak, sub-200ms matching latency, and 99.99% availability. Building even a simplified version of this system demonstrates proficiency in real-time systems, geospatial algorithms, event-driven architectures, state machines, and performance optimization. These are the skills that separate senior engineers from the rest.

Final Tip: In an interview, do not try to cover every component in equal depth. Focus on the 2-3 components most relevant to the interviewer's follow-up questions. Show depth over breadth, and always explain the trade-offs behind your design decisions. The best answers demonstrate not just what you would build, but why you would build it that way and what alternatives you considered.

Originally published on Ayodhyya. Last updated June 15, 2026.