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
Table of Contents
- Introduction - Why Ride-Sharing Is a Premier System Design Problem
- Functional and Non-Functional Requirements
- Capacity Estimation and Back-of-Envelope Math
- High-Level Architecture Overview
- Location Ingestion Pipeline and Geohash Indexing
- Proximity Search - Finding Nearby Drivers in Real-Time
- Matching Engine - From Ride Request to Driver Assignment
- Surge Pricing - Dynamic Fare Calculation
- Trip Lifecycle and State Machine
- Real-Time Trip Tracking with WebSockets
- Payment Processing and Fare Settlement
- Data Storage Architecture and Sharding Strategy
- Safety, Fraud Detection, and Anomaly Handling
- Caching, CDN, and Performance Optimization
- Multi-Region Deployment and Disaster Recovery
- Monitoring, Observability, and SLA Management
- Cost Estimation and Infrastructure Sizing
- Complete C# Implementation - Matching and Trip Services
- Interview Q&A - 25 Questions with Detailed Answers
- 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.
2. Functional and Non-Functional Requirements
Functional Requirements
| # | Requirement | Priority | Details |
|---|---|---|---|
| F1 | Rider requests a ride | Must | Rider specifies pickup/dropoff, selects ride type, sees fare estimate |
| F2 | Driver location tracking | Must | Drivers send GPS pings every 3-4 seconds, stored and indexed in real-time |
| F3 | Driver-rider matching | Must | Match nearest available driver within configurable radius using proximity search |
| F4 | Surge pricing | Must | Dynamic multiplier based on supply-demand ratio per geohash cell |
| F5 | Real-time trip tracking | Must | Rider sees driver ETA and live location on map during trip |
| F6 | Trip lifecycle management | Must | State machine: REQUESTED, ACCEPTED, IN_PROGRESS, COMPLETED, PAID |
| F7 | Payment processing | Must | Support credit card, digital wallets, and split fare |
| F8 | Driver availability toggle | Must | Driver can go online/offline, affecting real-time availability index |
| F9 | Ride history | Should | Both rider and driver can view past trips with routes and receipts |
| F10 | Rating system | Should | Bidirectional rating: rider rates driver, driver rates rider |
| F11 | Safety features | Should | SOS button, trip sharing, driver verification, PIN-based pickup |
| F12 | Multi-ride-type support | Nice | Economy, premium, pooled, wheelchair accessible vehicle options |
| F13 | Scheduled rides | Nice | Rider books a ride in advance for a future time |
| F14 | Internationalization | Nice | Multi-currency, multi-language support across regions |
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Matching latency | < 200ms (p99) | Riders expect near-instant driver assignment |
| Location ping ingestion | < 50ms (p99) | Real-time driver tracking requires sub-second writes |
| Availability | 99.99% | Downtime means zero rides, directly impacting revenue and safety |
| Consistency | Eventual (location), Strong (trip/payment) | Location data can be stale; trip state and money cannot |
| Throughput | 40K location writes/sec, 42K match QPS | Peak demand in dense urban areas |
| Data retention | Trip history: 5+ years, Location: 90 days | Regulatory and business requirements |
| Geo-redundancy | Active-active multi-region | Datacenter failure must not take down the platform |
| Fare accuracy | Estimate within 10% of final | User trust depends on predictable pricing |
3. Capacity Estimation and Back-of-Envelope Math
Daily Volume Estimates
| Metric | Calculation | Result |
|---|---|---|
| Daily active riders | Given | 10 million |
| Daily active drivers (peak) | Given | 1 million |
| Daily trips | Given | 15 million |
| Location pings per driver per day | 86,400 sec / 4 sec interval | 21,600 pings |
| Total location pings per day | 1M drivers x 21,600 | 21.6 billion pings |
| Location ping payload | driver_id + lat + lng + timestamp + metadata | ~500 bytes |
| Daily location data ingress | 21.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 storage | 15M x 2 KB per trip | ~30 GB/day |
| Annual trip storage | 30 GB x 365 | ~11 TB/year |
| Annual location storage | 10.8 TB x 90 days retention | ~972 TB (archived) |
Storage Breakdown by Service
| Service | Storage Type | Daily Growth | Retention |
|---|---|---|---|
| Location history | Cassandra (time-series) | 10.8 TB | 90 days |
| Trip data | PostgreSQL (sharded) | 30 GB | 5+ years |
| User profiles | PostgreSQL / MongoDB | ~1 GB | Indefinite |
| Real-time locations | Redis (ephemeral) | ~60 GB (in-memory) | 10 sec TTL |
| Driver earnings | PostgreSQL | ~5 GB | Indefinite |
| Payment transactions | PostgreSQL + Kafka | ~2 GB | 7 years (regulatory) |
| Analytics events | Kafka to ClickHouse | ~500 GB | 1 year |
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.
Service Responsibilities
| Service | Protocol | Responsibility | Latency Target |
|---|---|---|---|
| Location Service | gRPC / WebSocket | Ingest GPS pings, update Redis, persist to Cassandra | < 50ms |
| Matching Service | gRPC | Find nearest drivers, score candidates, send ride requests | < 200ms |
| Trip Service | gRPC / REST | Create trips, manage state machine, calculate fares | < 100ms |
| Pricing Service | gRPC | Compute surge multiplier, fare estimates | < 100ms |
| Payment Service | REST | Charge rider, pay driver, handle refunds | < 500ms |
| Notification Service | Async (Kafka) | Push notifications, SMS, email | < 5s |
| User Service | REST | Auth, 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
Geohash Precision Levels
| Precision | Cell Size (approx) | Use Case | Drivers per Cell (NYC) |
|---|---|---|---|
| Level 4 | 39 km x 20 km | City-level partitioning | ~50,000 |
| Level 5 | 5 km x 5 km | District-level matching | ~8,000 |
| Level 6 | 1.2 km x 0.6 km | Neighborhood matching | ~500 |
| Level 7 | 153m x 153m | Precise proximity search | ~15-50 |
| Level 8 | 38m x 19m | Street-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
| Approach | Lookup Time | Memory | Accuracy | Complexity |
|---|---|---|---|---|
| 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) avg | High | ~99% | Medium |
| Google S2 cells | O(log N) | Medium | ~100% | High |
| PostGIS spatial query | O(N) worst | Low | ~100% | Low |
| Linear scan (all drivers) | O(N) | None | 100% | Trivial |
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);
}
}
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 Ratio | Surge Multiplier | Estimated Fare ($25 base) | Behavior |
|---|---|---|---|
| < 1.0 | 1.0x | $25.00 | Normal pricing, slight discount possible |
| 1.0 - 1.5 | 1.0x | $25.00 | Balanced market, no surge |
| 1.5 - 2.0 | 1.2x | $30.00 | Mild surge, most riders accept |
| 2.0 - 3.0 | 1.5x | $37.50 | Moderate surge |
| 3.0 - 5.0 | 2.0x | $50.00 | High surge, strong demand signal |
| 5.0 - 8.0 | 3.0x | $75.00 | Very high surge, events or emergencies |
| > 8.0 | 5.0x | $125.00 | Extreme surge, maximum price cap applies |
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
Trip State Definitions
| State | Description | Valid Transitions | Side Effects |
|---|---|---|---|
| REQUESTED | Rider submitted ride request | MATCHING, CANCELLED | Broadcast to matching service |
| MATCHING | Searching for drivers | DRIVER_FOUND, CANCELLED | Notify nearby drivers |
| DRIVER_FOUND | Driver assigned, awaiting acceptance | ACCEPTED, CANCELLED | Push notification to driver |
| ACCEPTED | Driver confirmed the ride | DRIVER_EN_ROUTE, CANCELLED | Share driver details with rider |
| DRIVER_EN_ROUTE | Driver heading to pickup | ARRIVED, CANCELLED | Real-time tracking enabled |
| ARRIVED | Driver at pickup location | IN_PROGRESS | Notify rider, start timer |
| IN_PROGRESS | Trip in progress | COMPLETED, EMERGENCY | Live tracking, route monitoring |
| COMPLETED | Trip ended at destination | PAID | Calculate final fare, process payment |
| PAID | Payment processed | RATED | Send receipt, update earnings |
| CANCELLED | Trip cancelled | Terminal | Cancel 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
| Metric | Target | Measurement |
|---|---|---|
| Location update latency | < 500ms end-to-end | Driver GPS to Rider screen |
| WebSocket connections | 5M+ concurrent | Across all gateway servers |
| Messages per second | 125K+ location updates | At peak load |
| Connection establishment | < 200ms | TLS handshake + upgrade |
| Reconnection time | < 2 seconds | After network interruption |
| Protocol overhead | < 50 bytes per message | Protocol Buffers vs 500 bytes JSON |
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
Fare Calculation Components
| Component | Economy | Comfort | Premium | Calculation |
|---|---|---|---|---|
| Base fare | $2.50 | $4.00 | $7.00 | Fixed charge per trip |
| Per km | $1.00 | $1.50 | $2.50 | Distance x rate |
| Per minute | $0.20 | $0.30 | $0.50 | Duration x rate |
| Booking fee | $1.50 | $2.00 | $2.50 | Fixed platform fee |
| Minimum fare | $5.00 | $7.00 | $12.00 | Floor price |
| Cancellation (5 min+) | $5.00 | $7.00 | $10.00 | After 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 Type | Database | Partition Key | Replication | Retention |
|---|---|---|---|---|
| Real-time driver locations | Redis Cluster | Geohash6 prefix | 3 replicas per shard | 10 seconds (TTL) |
| Location history | Cassandra | (driver_id, date) | RF=3 | 90 days |
| Trip records | PostgreSQL (sharded) | trip_id (hash) | Primary + read replica | 5+ years |
| User profiles | PostgreSQL | user_id | Primary + 2 read replicas | Indefinite |
| Driver earnings | PostgreSQL | driver_id | Primary + read replica | Indefinite |
| Payment transactions | PostgreSQL | payment_id | Primary + 2 read replicas | 7 years |
| Analytics events | Kafka to ClickHouse | Date-based partition | RF=2 | 1 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
| Service | Write QPS | Read QPS | Consistency | Access Pattern |
|---|---|---|---|---|
| Location Service | 125K | 50K | Eventual | Write: latest ping. Read: Redis sorted set by geohash |
| Trip Service | 5K | 20K | Strong | Write: state transitions. Read: by trip_id, rider_id |
| Matching Service | 100 | 5K | Eventual | Write: match result. Read: Redis driver locations |
| Pricing Service | 1K | 10K | Eventual | Write: surge values. Read: fare estimates by geohash |
| Payment Service | 200 | 1K | Strong | Write: 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
| Feature | Implementation | Latency | Priority |
|---|---|---|---|
| SOS Button | Immediate alert to safety team + local emergency services | < 2 sec | Critical |
| Trip sharing | Rider shares live trip link with trusted contacts | < 5 sec | High |
| Route deviation | ML model flags unusual route deviations during trip | < 30 sec | High |
| Unusual stop detection | Flag stops longer than 2 minutes during active trip | < 2 min | Medium |
| Speed anomaly | Flag excessive speed or sudden braking events | < 10 sec | Medium |
| Identity verification | Driver selfie verification at shift start | < 5 sec | High |
| PIN-based pickup | Rider shows PIN to driver to verify correct ride | Instant | Medium |
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
| Layer | Technology | What is Cached | TTL | Hit Rate Target |
|---|---|---|---|---|
| L1 (In-Process) | MemoryCache | Pricing config, city settings | 5 minutes | > 99% |
| L2 (Distributed) | Redis Cluster | Driver locations, surge values, sessions | 10 sec - 24 hr | > 95% |
| L3 (CDN) | CloudFront | Static assets, map tiles, app bundles | 1 hour - 7 days | > 90% |
| L4 (Database) | PostgreSQL read replicas | Trip history, user profiles | Continuous sync | N/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.
Regional Routing Table
| Region | Cities | Peak QPS | Drivers | Redis Shards |
|---|---|---|---|---|
| US-East (Virginia) | New York, Boston, Miami, Chicago | 15,000 | 800K | 24 |
| US-West (Oregon) | Los Angeles, San Francisco, Seattle | 12,000 | 600K | 18 |
| EU-West (Ireland) | London, Paris, Berlin, Amsterdam | 18,000 | 1M | 30 |
| AP-South (Mumbai) | Mumbai, Delhi, Bangalore | 20,000 | 1.5M | 36 |
| AP-East (Singapore) | Singapore, Tokyo, Sydney | 10,000 | 500K | 15 |
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
| Metric | SLA Target | Measurement | Alert Threshold |
|---|---|---|---|
| Matching success rate | > 98% | Trips matched / ride requests | < 95% over 5 min |
| Matching latency (p99) | < 200ms | Request 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 availability | 99.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 stale | Max 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
| Component | Spec | Count | Monthly Cost |
|---|---|---|---|
| Application servers | c5.2xlarge (8 vCPU, 16GB) | 200 | $62,000 |
| WebSocket gateway servers | c5.4xlarge (16 vCPU, 32GB) | 100 | $48,000 |
| Redis cluster | r6g.2xlarge (8 vCPU, 52GB) | 150 | $85,000 |
| PostgreSQL (256 shards + replicas) | r5.2xlarge (8 vCPU, 64GB) | 512 | $245,000 |
| Cassandra | i3.2xlarge (8 vCPU, 61GB SSD) | 100 | $78,000 |
| Kafka cluster | m5.2xlarge (8 vCPU, 32GB) | 30 | $18,000 |
| Data transfer | ~50 TB/month | N/A | $15,000 |
| Mapping API | 100M API calls/month | N/A | $50,000 |
| Payment gateway fees | 2.9% + $0.30 per txn | 450M/month | $2,500,000 |
| Push notifications | 500M messages/month | N/A | $5,000 |
| Monitoring | Datadog / Grafana Cloud | N/A | $25,000 |
| Total estimated monthly | ~$3,131,000 |
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
| From | To | Protocol | Pattern | SLA |
|---|---|---|---|---|
| Rider App | API Gateway | HTTPS/REST | Synchronous | < 100ms |
| Driver App | WebSocket GW | WSS | Long-lived connection | < 500ms |
| API Gateway | Trip Service | gRPC | Synchronous | < 100ms |
| Trip Service | Matching Service | gRPC | Synchronous | < 200ms |
| Matching Service | Location Service | gRPC | Synchronous | < 50ms |
| Location Service | Redis | Redis protocol | Synchronous | < 5ms |
| Location Service | Kafka | Kafka protocol | Async fire-and-forget | < 10ms |
| Kafka | Notification Service | Kafka consumer | Async pull | < 5s |
| Payment Service | Stripe/Braintree | HTTPS/REST | Synchronous | < 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.