How to Design Ride Sharing Platform like Uber
Building real-time matching, dynamic pricing, and GPS tracking at 30M+ daily ride scale
1. Introduction — Uber at Global Scale
Uber is the world's largest ride-hailing platform, operating across 10,000+ cities in 72 countries. Every single day, Uber processes over 30 million trips, connects 5 million+ drivers with riders, and handles billions of GPS data points in real time. The platform must match a rider to the nearest available driver in under 5 seconds, compute dynamic pricing every 2 minutes, stream live GPS locations to millions of concurrent users, and process payments across dozens of currencies and payment methods — all while maintaining sub-second p99 latency.
The core technical challenges in building such a platform include: real-time geospatial indexing for driver discovery, a fair and efficient matching algorithm that minimizes wait times, dynamic pricing that balances supply and demand without alienating users, reliable WebSocket connections for live trip tracking, and globally distributed data storage with strong consistency where it matters and eventual consistency elsewhere. In this article, we will design every major subsystem from the ground up, targeting a 30M+ daily ride scale, with concrete C# code, database schemas, and architecture diagrams.
Uber's engineering organization has published extensively about their technical evolution — from a Ruby monolith to a microservices architecture handling 1.3 million requests per second at peak. The platform ingests over 2 million GPS events per second, runs machine learning models for ETA prediction with sub-10% error rates, and operates one of the largest real-time messaging infrastructures in the world. This article distills these lessons into a comprehensive system design that you can present in a senior+ engineering interview or use as a reference architecture for building similar platforms.
2. Requirements
Functional Requirements
- Rider can request a ride — specify pickup, dropoff, ride type (UberX, Black, Pool)
- System matches a driver — find nearest available driver within configurable radius
- Real-time GPS tracking — rider sees driver approach and trip progress live
- Dynamic / surge pricing — adjust prices based on supply-demand in real time
- ETA calculation — accurate arrival time using traffic-aware routing
- Payment processing — cashless payments, split fare, tipping
- Rating system — bidirectional rider-driver ratings
- Trip history — view all past trips with receipts
- Safety features — SOS button, trip sharing, audio recording
- Driver onboarding — registration, document upload, background check
- Ride pooling — UberX Share with route-matched riders
- Notifications — push, SMS, and email for trip lifecycle events
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Availability | 99.99% (52 min downtime/year) | Revenue loss of ~$150K/min at Uber's scale |
| Latency — Match | < 5 seconds (p99) | Riders won't wait; competitor switch risk |
| Latency — GPS Ingestion | < 200ms (p99) | Stale location = bad matches |
| Throughput | ~350K trips/sec peak | Friday evening rush across all cities |
| GPS Writes | ~2M location updates/sec | 5M drivers × 2 updates/sec avg |
| Data Durability | 99.999999999% (11 nines) | Trip and payment data must never be lost |
| Consistency | Strong for payments, eventual for locations | Money must be exact; location can lag 1-2s |
| Geo Queries | Radius search < 50ms | Matching algorithm depends on fast proximity |
3. Capacity Estimation & Back-of-Envelope
Traffic Estimation
Daily Rides: 30,000,000
Average QPS: 30M / 86,400 ≈ 347 trips/sec
Peak QPS (15% of daily in 4 hours): 4.5M / 14,400 ≈ 31,250 trips/sec (burst: ~50K/sec)
Average Trip Duration: 20 minutes
Concurrent Active Trips: 30M × (20/1440) ≈ 416,667 concurrent trips at any instant
Storage Estimation
| Data Type | Size per Record | Daily Volume | Daily Storage | Annual |
|---|---|---|---|---|
| Trip Records | 2 KB | 30M | 60 GB | 22 TB |
| GPS Points (raw) | 64 bytes | 15B (500/driver) | ~1 TB | 365 TB |
| Payments | 1 KB | 30M | 30 GB | 11 TB |
| User Profiles | 2 KB | Incremental | ~1 GB | 365 GB |
| Ratings | 256 bytes | 30M | 7.5 GB | 2.7 TB |
Key Insight: GPS point storage dominates. Uber uses a tiered storage strategy — hot GPS data (last 24 hours) in Redis/Cassandra, warm data (24h–30d) in compressed Cassandra SSTables, and cold data (30d+) in Parquet files on S3 for analytics. This reduces storage cost by ~80% compared to keeping everything hot.
Bandwidth Estimation
Inbound (GPS): 5M drivers × 2 updates/sec × 64 bytes = 640 MB/sec
Outbound (location push): ~1M concurrent rider connections × 512 bytes/sec = 512 MB/sec
Total peak bandwidth: ~2 Gbps sustained, ~8 Gbps burst
4. Data Model Design
Entity Relationship Overview
Riders Table
CREATE TABLE riders (
rider_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20) UNIQUE NOT NULL,
full_name VARCHAR(100) NOT NULL,
profile_photo VARCHAR(500),
default_payment UUID REFERENCES payment_methods(payment_id),
rating_avg DECIMAL(3,2) DEFAULT 5.00,
total_trips INT DEFAULT 0,
country_code VARCHAR(3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_riders_phone ON riders(phone);
CREATE INDEX idx_riders_country ON riders(country_code);
Drivers Table
CREATE TABLE drivers (
driver_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20) UNIQUE NOT NULL,
full_name VARCHAR(100) NOT NULL,
license_number VARCHAR(50) UNIQUE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'offline',
current_lat DOUBLE PRECISION,
current_lng DOUBLE PRECISION,
heading SMALLINT,
speed_kmh SMALLINT,
rating_avg DECIMAL(3,2) DEFAULT 5.00,
total_trips INT DEFAULT 0,
earnings_total DECIMAL(12,2) DEFAULT 0,
city_id UUID REFERENCES cities(city_id),
h3_index VARCHAR(20),
last_location_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_drivers_status ON drivers(status);
CREATE INDEX idx_drivers_h3 ON drivers(h3_index);
CREATE INDEX idx_drivers_city ON drivers(city_id);
CREATE INDEX idx_drivers_location ON drivers
USING GIST (ST_Point(current_lng, current_lat));
Trips Table
CREATE TABLE trips (
trip_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rider_id UUID NOT NULL REFERENCES riders(rider_id),
driver_id UUID REFERENCES drivers(driver_id),
status VARCHAR(20) NOT NULL DEFAULT 'requested',
ride_type VARCHAR(20) NOT NULL,
pickup_lat DOUBLE PRECISION NOT NULL,
pickup_lng DOUBLE PRECISION NOT NULL,
pickup_address VARCHAR(500),
dropoff_lat DOUBLE PRECISION NOT NULL,
dropoff_lng DOUBLE PRECISION NOT NULL,
dropoff_address VARCHAR(500),
estimated_distance_km DECIMAL(6,2),
actual_distance_km DECIMAL(6,2),
estimated_duration_sec INT,
actual_duration_sec INT,
base_fare DECIMAL(10,2),
surge_multiplier DECIMAL(4,2) DEFAULT 1.00,
total_fare DECIMAL(10,2),
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
payment_method VARCHAR(20),
payment_status VARCHAR(20) DEFAULT 'pending',
requested_at TIMESTAMPTZ DEFAULT NOW(),
matched_at TIMESTAMPTZ,
arrived_at TIMESTAMPTZ,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
cancel_reason VARCHAR(500),
cancelled_by VARCHAR(10)
);
CREATE INDEX idx_trips_rider ON trips(rider_id, requested_at DESC);
CREATE INDEX idx_trips_driver ON trips(driver_id, requested_at DESC);
CREATE INDEX idx_trips_status ON trips(status);
Location Updates Table
CREATE TABLE driver_locations (
driver_id UUID NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lng DOUBLE PRECISION NOT NULL,
heading SMALLINT,
speed_kmh SMALLINT,
accuracy_m REAL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (driver_id, recorded_at)
) PARTITION BY RANGE (recorded_at);
CREATE TABLE driver_locations_2026_07
PARTITION OF driver_locations
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
Payments Table
CREATE TABLE payments (
payment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trip_id UUID NOT NULL REFERENCES trips(trip_id),
rider_id UUID NOT NULL,
driver_id UUID NOT NULL,
amount DECIMAL(10,2) NOT NULL,
currency VARCHAR(3) NOT NULL,
platform_fee DECIMAL(10,2) NOT NULL,
driver_payout DECIMAL(10,2) NOT NULL,
tip_amount DECIMAL(10,2) DEFAULT 0,
payment_method VARCHAR(20) NOT NULL,
payment_gateway VARCHAR(30),
gateway_txn_id VARCHAR(100),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT NOW(),
settled_at TIMESTAMPTZ
);
CREATE INDEX idx_payments_trip ON payments(trip_id);
CREATE INDEX idx_payments_driver_settle ON payments(driver_id, settled_at);
5. API Design
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/v1/trips/request | Request a new ride | Bearer JWT |
| GET | /api/v1/trips/{tripId} | Get trip details and status | Bearer JWT |
| POST | /api/v1/trips/{tripId}/cancel | Cancel active trip | Bearer JWT |
| PUT | /api/v1/trips/{tripId}/rate | Rate completed trip | Bearer JWT |
| POST | /api/v1/trips/{tripId}/safety/sos | Trigger SOS alert | Bearer JWT |
| POST | /api/v1/drivers/location | Report GPS location | Driver JWT |
| PUT | /api/v1/drivers/status | Go online/offline | Driver JWT |
| POST | /api/v1/drivers/trips/{tripId}/accept | Accept matched trip | Driver JWT |
| PUT | /api/v1/drivers/trips/{tripId}/arrived | Mark arrival at pickup | Driver JWT |
| POST | /api/v1/drivers/trips/{tripId}/start | Start trip | Driver JWT |
| POST | /api/v1/drivers/trips/{tripId}/complete | Complete trip | Driver JWT |
| GET | /api/v1/riders/trips/history | Paginated trip history | Bearer JWT |
| GET | /api/v1/pricing/estimate | Fare estimate for route | Bearer JWT |
| GET | /api/v1/eta | ETA to pickup for rider | Bearer JWT |
Request a Ride — Request/Response
// POST /api/v1/trips/request
{
"pickup": {
"lat": 37.7749,
"lng": -122.4194,
"address": "1 Market St, San Francisco, CA"
},
"dropoff": {
"lat": 37.8044,
"lng": -122.2712,
"address": "Oakland International Airport"
},
"ride_type": "uberx",
"payment_method_id": "pm_abc123"
}
// Response 201 Created
{
"trip_id": "trip_9f8e7d6c",
"status": "requested",
"eta_seconds": 240,
"surge_multiplier": 1.3,
"price_estimate": { "min": 28.50, "max": 35.00, "currency": "USD" },
"matching_drivers_count": 12
}
WebSocket Protocol
// Connection
wss://rt.uber.com/v1/trips/{trip_id}?token={jwt_token}
// Server to Client — Location Update
{
"type": "location_update",
"data": {
"lat": 37.7801, "lng": -122.4108,
"heading": 45, "speed_kmh": 32,
"timestamp": "2026-07-14T10:32:15Z"
}
}
// Server to Client — Status Change
{
"type": "status_change",
"data": {
"status": "driver_enroute",
"eta_seconds": 180,
"driver": {
"name": "John D.",
"vehicle": "Toyota Camry White",
"plate": "7ABC123",
"rating": 4.92
}
}
}
// Client to Server — Heartbeat
{ "type": "ping", "timestamp": "2026-07-14T10:32:16Z" }
6. High-Level Architecture
Architecture Philosophy: Uber migrated from a monolith to a microservices architecture starting in 2012, completing the transition by 2018. Each service owns its data, communicates via Kafka events for async workflows, and uses gRPC for synchronous inter-service calls. The API Gateway handles authentication, rate limiting, and request routing based on geohash of the user's location to route to the nearest regional cluster.
7. Location Service — GPS Ingestion & H3 Hex Grid
The Location Service is the backbone of the entire platform. It must ingest ~2 million GPS updates per second from 5 million active drivers, store them durably, and support sub-50ms radius queries for the matching algorithm. Uber's innovation here is using the Uber H3 hexagonal hierarchical geospatial indexing system, which divides the Earth into hexagonal cells at 16 resolution levels.
Why H3 Hexagonal Grid?
| Property | H3 Hex Grid | Traditional Lat/Lng |
|---|---|---|
| Neighbor Search | O(1) — 6 fixed neighbors | O(n) — bounding box query |
| Equal Area | Hexagons have ~equal area | Rectangles distort at latitudes |
| Hierarchy | Resolution 0-15, parent-child | No inherent hierarchy |
| Indexing | Single 64-bit integer | Two doubles (lat, lng) |
| Edge Effects | Minimal — hexagonal packing | Significant at grid boundaries |
H3 Resolution Strategy
| H3 Resolution | Hex Edge | Hex Area | Use Case |
|---|---|---|---|
| Level 3 | ~110 km | ~36,000 km² | City-level pricing zones |
| Level 5 | ~11 km | ~360 km² | Surge pricing grid |
| Level 7 | ~1.2 km | ~4 km² | Driver clustering |
| Level 8 | ~460 m | ~0.55 km² | Matching radius |
| Level 10 | ~67 m | ~0.012 km² | Pickup point precision |
Key Design Decision: When a rider requests a ride, we do not query all drivers within a radius using expensive geospatial queries. Instead, we look up the rider's H3 cell at resolution 8, then check the 6 neighboring hexes plus the rider's own cell. Drivers in those cells are stored in Redis sorted sets keyed by H3 cell, with scores based on their last GPS timestamp. This converts an expensive spatial query into a simple Redis lookup — O(1) for the index, O(k) for retrieval where k is drivers per cell.
Location Ingestion Service — C# Implementation
public class LocationIngestionService : BackgroundService
{
private readonly IKafkaProducer<string, DriverLocationDto> _kafkaProducer;
private readonly IDriverLocationCache _redisCache;
private readonly IH3IndexService _h3Service;
private readonly ILogger<LocationIngestionService> _logger;
private readonly ConcurrentBag<DriverLocationDto> _batch = new();
private const int FLUSH_INTERVAL_MS = 200;
protected override async Task ExecuteAsync(CancellationToken ct)
{
var timer = PeriodicTimer(TimeSpan.FromMilliseconds(FLUSH_INTERVAL_MS));
while (await timer.WaitForNextTickAsync(ct))
{
if (_batch.IsEmpty) continue;
var items = _batch.ToArray();
_batch.Clear();
try
{
var kafkaTasks = items.Select(loc =>
_kafkaProducer.ProduceAsync("driver-locations",
loc.DriverId, loc, ct));
await Task.WhenAll(kafkaTasks);
var redisTasks = items
.GroupBy(l => l.DriverId)
.Select(g => g.OrderByDescending(l => l.Timestamp).First())
.Select(loc =>
{
var h3Index = _h3Service.ToH3(
loc.Lat, loc.Lng, resolution: 8);
return _redisCache.UpdateDriverLocationAsync(
loc.DriverId, loc.Lat, loc.Lng,
h3Index, loc.Heading, loc.SpeedKmh);
});
await Task.WhenAll(redisTasks);
_logger.LogDebug("Flushed {Count} location updates", items.Length);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to flush location batch of {Count}", items.Length);
}
}
}
public void Enqueue(DriverLocationDto location) => _batch.Add(location);
}
public class H3IndexService : IH3IndexService
{
private readonly H3Api _h3Api;
public string ToH3(double lat, double lng, int resolution)
{
var geoCoord = new GeoCoord(lat, lng);
var h3Index = _h3Api.GeoToH3(ref geoCoord, resolution);
return h3Index.ToString();
}
public List<string> GetRing(string centerH3, int ringSize = 1)
{
var center = ulong.Parse(centerH3);
return _h3Api.GridRing(center, ringSize)
.Select(h => h.ToString()).ToList();
}
public List<string> GetDisk(string centerH3, int k = 1)
{
var center = ulong.Parse(centerH3);
return _h3Api.GridDisk(center, k)
.Select(h => h.ToString()).ToList();
}
}
8. Driver Availability & Matching Algorithm
The matching algorithm is Uber's core competitive advantage. It must simultaneously optimize for minimized rider wait time, maximized driver utilization, fairness across drivers, and minimized detour for pooled rides. At scale, the system processes ~50,000 match requests per second at peak.
Matching Score Formula
Match Score = W1 x Proximity + W2 x Rating + W3 x AcceptanceRate - W4 x CancellationRate - W5 x SurgeGap
- W1 (Proximity, 0.40): Inverse distance; drivers closer to pickup score higher. Normalized to 0-1 within search radius.
- W2 (Rating, 0.20): Driver rating / 5.0. New drivers start at 0.8 baseline to bootstrap.
- W3 (Acceptance Rate, 0.15): Rolling 30-day trip acceptance rate.
- W4 (Cancellation Rate, 0.15): Penalizes drivers who cancel frequently after accepting.
- W5 (Surge Gap, 0.10): Reduces score if driver's expected earnings significantly exceed the surge.
C# Matching Algorithm Implementation
public class MatchingService : IMatchingService
{
private readonly IDriverLocationCache _locationCache;
private readonly IH3IndexService _h3;
private readonly IMatchConfig _config;
private const int SEARCH_HEX_RING_SIZE = 2;
private const int MAX_CANDIDATES = 20;
private const int MATCH_TIMEOUT_SECONDS = 30;
private static readonly MatchWeights Weights = new()
{
Proximity = 0.40, Rating = 0.20, AcceptanceRate = 0.15,
CancellationRate = 0.15, SurgeGap = 0.10
};
public async Task<MatchResult?> FindBestDriverAsync(
RideRequest request, CancellationToken ct)
{
var pickupH3 = _h3.ToH3(request.PickupLat, request.PickupLng, 8);
var searchCells = _h3.GetDisk(pickupH3, SEARCH_HEX_RING_SIZE);
var driverTasks = searchCells.Select(cell =>
_locationCache.GetDriversInCellAsync(cell, request.RideType));
var driverArrays = await Task.WhenAll(driverTasks);
var candidates = driverArrays.SelectMany(d => d)
.Where(d => d.Status == DriverStatus.Available
&& d.LastLocationUpdate > DateTime.UtcNow.AddMinutes(-2))
.DistinctBy(d => d.DriverId)
.Take(MAX_CANDIDATES).ToList();
if (!candidates.Any()) return null;
var scored = candidates
.Select(d => new { Driver = d, Score = CalculateMatchScore(d, request) })
.OrderByDescending(x => x.Score).ToList();
foreach (var candidate in scored)
{
var accepted = await RequestDriverAcceptanceAsync(
candidate.Driver.DriverId, request.TripId,
TimeSpan.FromSeconds(MATCH_TIMEOUT_SECONDS), ct);
if (accepted)
{
return new MatchResult
{
DriverId = candidate.Driver.DriverId,
MatchScore = candidate.Score,
DriverLat = candidate.Driver.CurrentLat,
DriverLng = candidate.Driver.CurrentLng,
EstimatedArrival = CalculateDriverEta(candidate.Driver, request)
};
}
}
return null;
}
private double CalculateMatchScore(DriverInfo driver, RideRequest request)
{
var distM = HaversineDistance(
driver.CurrentLat, driver.CurrentLng,
request.PickupLat, request.PickupLng);
var proximityScore = Math.Max(0,
1.0 - (distM / _config.MaxSearchRadiusMeters));
var ratingScore = driver.RatingAvg / 5.0;
var acceptanceScore = driver.AcceptanceRate;
var cancellationScore = 1.0 - driver.CancellationRate;
var surgeGapScore = 1.0 - Math.Min(1.0,
Math.Abs(driver.ExpectedSurge - request.SurgeMultiplier) / 3.0);
return Math.Round(
Weights.Proximity * proximityScore +
Weights.Rating * ratingScore +
Weights.AcceptanceRate * acceptanceScore +
Weights.CancellationRate * cancellationScore +
Weights.SurgeGap * surgeGapScore, 4);
}
private static double HaversineDistance(
double lat1, double lng1, double lat2, double lng2)
{
const double R = 6371000;
var dLat = ToRadians(lat2 - lat1);
var dLng = ToRadians(lng2 - lng1);
var 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 static double ToRadians(double deg) => deg * Math.PI / 180.0;
}
9. Trip State Machine
A trip transitions through a well-defined set of states. Every state transition triggers downstream events — notifications, payment holds, driver earnings calculations, and analytics ingestion.
State Transition Rules
| Transition | Trigger | Side Effects |
|---|---|---|
| Requested to Matching | Rider submits request | Hold rider payment method, notify nearby drivers |
| Matching to Matched | Driver accepts within 30s | Notify rider with driver details, ETA |
| Matching to NoDrivers | No driver accepted in 120s | Release payment hold, offer retry or expand search |
| Matched to DriverEnroute | Driver acknowledges | Start real-time tracking stream |
| DriverEnroute to Arrived | Driver within 50m of pickup | Notify rider, start 5-min timer |
| Arrived to InProgress | Driver taps Start Trip | Begin fare calculation, start billing |
| InProgress to Completed | Driver taps End Trip | Calculate final fare, process payment, request ratings |
C# Trip State Machine
public class TripStateMachine
{
private static readonly Dictionary<TripStatus, HashSet<TripStatus>>
AllowedTransitions = new()
{
[TripStatus.Requested] = new()
{ TripStatus.Matching, TripStatus.CancelledRider },
[TripStatus.Matching] = new()
{ TripStatus.Matched, TripStatus.CancelledRider,
TripStatus.CancelledDriver, TripStatus.NoDrivers },
[TripStatus.Matched] = new()
{ TripStatus.DriverEnroute, TripStatus.CancelledRider,
TripStatus.CancelledDriver },
[TripStatus.DriverEnroute] = new()
{ TripStatus.Arrived, TripStatus.CancelledRider,
TripStatus.CancelledDriver },
[TripStatus.Arrived] = new()
{ TripStatus.InProgress, TripStatus.CancelledRider },
[TripStatus.InProgress] = new()
{ TripStatus.Completed, TripStatus.CancelledDriver }
};
private readonly ITripRepository _tripRepo;
private readonly IEventBus _eventBus;
public async Task<TransitionResult> TransitionAsync(
Guid tripId, TripStatus newStatus,
string triggeredBy, string? reason = null)
{
var trip = await _tripRepo.GetByIdAsync(tripId);
if (trip == null)
return TransitionResult.Fail("Trip not found");
if (!AllowedTransitions.ContainsKey(trip.Status) ||
!AllowedTransitions[trip.Status].Contains(newStatus))
return TransitionResult.Fail(
$"Invalid transition: {trip.Status} to {newStatus}");
var previousStatus = trip.Status;
trip.Status = newStatus;
trip.UpdatedAt = DateTime.UtcNow;
switch (newStatus)
{
case TripStatus.Matched:
trip.MatchedAt = DateTime.UtcNow;
trip.DriverId = Guid.Parse(triggeredBy);
break;
case TripStatus.Arrived:
trip.ArrivedAt = DateTime.UtcNow;
break;
case TripStatus.InProgress:
trip.StartedAt = DateTime.UtcNow;
break;
case TripStatus.Completed:
trip.CompletedAt = DateTime.UtcNow;
break;
case TripStatus.CancelledRider:
case TripStatus.CancelledDriver:
trip.CancelledAt = DateTime.UtcNow;
trip.CancelReason = reason;
trip.CancelledBy = newStatus == TripStatus.CancelledRider
? "rider" : "driver";
break;
}
await _tripRepo.SaveAsync(trip);
await _eventBus.PublishAsync(new TripStateChangedEvent
{
TripId = tripId, PreviousStatus = previousStatus,
NewStatus = newStatus, TriggeredBy = triggeredBy,
Timestamp = DateTime.UtcNow
});
return TransitionResult.Ok(previousStatus, newStatus);
}
}
10. Dynamic / Surge Pricing
Dynamic pricing balances supply and demand in real time. When demand exceeds supply in a geographic area, prices increase to attract more drivers and moderate demand. Surge is recalculated every 2 minutes per geographic zone using a model that considers historical demand, real-time request volume, active driver count, and upcoming events.
Surge Pricing Algorithm
| Metric | Formula | Example |
|---|---|---|
| Demand-Supply Ratio | D = requests/min / available_drivers | 120 / 40 = 3.0 |
| Target Utilization | U = 1 - (1 / D) | 1 - (1/3.0) = 67% |
| Surge Multiplier | S = Base x (1 + alpha x log(D)) x EventFactor x WeatherFactor | 1.0 x (1 + 0.3 x log(3.0)) x 1.1 x 1.15 = 1.52x |
| Surge Cap | min(S, MaxSurge) | min(1.52, 5.0) = 1.52x |
Anti-Oscillation: To prevent surge from rapidly toggling on/off, we apply a hysteresis mechanism: surge activates when D > 1.5 but deactivates only when D < 1.2. Additionally, surge changes are rate-limited to max 1 step per 2-minute window (each step = 0.1x increment).
C# Surge Pricing Calculator
public class SurgePricingService : ISurgePricingService
{
private readonly IDemandAggregator _demandAgg;
private readonly ISupplyAggregator _supplyAgg;
private readonly IWeatherService _weather;
private readonly IEventService _events;
private readonly IDistributedCache _cache;
private const double MAX_SURGE = 5.0;
private const double ACTIVATION_THRESHOLD = 1.5;
private const double DEACTIVATION_THRESHOLD = 1.2;
private const double ALPHA = 0.3;
public async Task<SurgeResult> CalculateSurgeAsync(
string h3Zone, string cityId)
{
var demand = await _demandAgg.GetRequestsPerMinuteAsync(h3Zone);
var supply = await _supplyAgg.GetAvailableDriversAsync(h3Zone);
var weather = await _weather.GetCurrentAsync(cityId);
var events = await _events.GetActiveEventsAsync(h3Zone);
var ratio = supply > 0 ? demand / (double)supply : 10.0;
var previousSurge = await _cache.GetAsync<double>(
$"surge:{h3Zone}") ?? 1.0;
double newSurge;
if (previousSurge > 1.0)
newSurge = ratio < DEACTIVATION_THRESHOLD
? 1.0 : CalculateMultiplier(ratio, weather, events);
else
newSurge = ratio > ACTIVATION_THRESHOLD
? CalculateMultiplier(ratio, weather, events) : 1.0;
var maxChange = 0.1;
newSurge = Math.Max(previousSurge - maxChange,
Math.Min(previousSurge + maxChange, newSurge));
newSurge = Math.Round(
Math.Max(1.0, Math.Min(MAX_SURGE, newSurge)), 1);
await _cache.SetAsync($"surge:{h3Zone}", newSurge,
TimeSpan.FromMinutes(5));
return new SurgeResult
{
Zone = h3Zone, Multiplier = newSurge,
DemandCount = demand, SupplyCount = supply,
CalculatedAt = DateTime.UtcNow,
RiderPriceMultiplier = newSurge,
DriverBonusMultiplier = Math.Max(1.0, newSurge * 0.8)
};
}
private double CalculateMultiplier(
double ratio, WeatherInfo weather, List<ActiveEvent> events)
{
var eventFactor = events.Any()
? events.Max(e => e.ImpactFactor) : 1.0;
var weatherFactor = weather.Condition switch
{
WeatherCondition.HeavyRain => 1.30,
WeatherCondition.Snow => 1.25,
WeatherCondition.LightRain => 1.15,
WeatherCondition.Fog => 1.08,
_ => 1.0
};
return 1.0 * (1 + ALPHA * Math.Log(Math.Max(1, ratio)))
* eventFactor * weatherFactor;
}
}
11. ETA Prediction & Route Optimization
Accurate ETA prediction directly impacts rider satisfaction and driver utilization. Uber uses a multi-layer approach: a fast geometric estimation for initial display (sub-10ms), refined by a graph-based routing engine (sub-100ms), and periodically updated by an ML model that accounts for real-time traffic, road conditions, and historical patterns.
ETA Layers
| Layer | Latency | Accuracy | Method |
|---|---|---|---|
| L1 Geometric | <10ms | +/-40% | Haversine / avg speed zone table |
| L2 Graph | <100ms | +/-15% | Shortest path on road graph (OSRM) |
| L3 ML Refined | <200ms | +/-8% | Gradient-boosted tree with traffic features |
| L4 Real-Time | 2-5s | +/-5% | Live GPS probe data + Kalman filter |
12. Real-Time Location Tracking (WebSocket)
Real-time tracking requires bidirectional low-latency communication between the server, rider app, and driver app. Uber uses WebSocket connections for rider tracking and MQTT for driver GPS streaming. At peak, the system maintains ~1 million concurrent WebSocket connections per city.
WebSocket Gateway: Each gateway instance maintains ~50K concurrent connections using a lightweight epoll model. The L4 LB ensures all messages for a given connection route to the same instance using consistent hashing on the trip ID. Heartbeat: clients send ping every 30 seconds. If no pong in 60s, the connection is terminated. Backpressure: when a gateway exceeds 45K connections, it signals the LB to stop routing new connections. Location data is inherently lossy — freshness matters more than completeness.
C# WebSocket Hub Implementation
public class TripTrackingHub : Hub
{
private readonly IDriverLocationCache _locationCache;
private readonly ITripService _tripService;
public override async Task OnConnectedAsync()
{
var tripId = Context.GetHttpContext()?
.Request.Query["trip_id"].ToString();
if (string.IsNullOrEmpty(tripId) ||
!Guid.TryParse(tripId, out var tripGuid))
{ Context.Abort(); return; }
var trip = await _tripService.GetAsync(tripGuid);
var userId = Context.UserIdentifier;
if (trip == null || (trip.RiderId.ToString() != userId
&& trip.DriverId?.ToString() != userId))
{ Context.Abort(); return; }
await Groups.AddToGroupAsync(
Context.ConnectionId, $"trip:{tripId}");
if (trip.DriverId.HasValue)
{
var loc = await _locationCache
.GetLocationAsync(trip.DriverId.Value);
if (loc != null)
await Clients.Caller.SendAsync("location_update", new
{
lat = loc.Lat, lng = loc.Lng,
heading = loc.Heading, speed_kmh = loc.SpeedKmh,
timestamp = DateTime.UtcNow
});
}
await base.OnConnectedAsync();
}
public async Task BroadcastDriverLocationAsync(
Guid tripId, double lat, double lng, int heading, int speedKmh)
{
await Clients.Group($"trip:{tripId}")
.SendAsync("location_update", new
{
lat, lng, heading, speed_kmh = speedKmh,
timestamp = DateTime.UtcNow
});
}
public async Task BroadcastStatusChangeAsync(
Guid tripId, string status, object data)
{
await Clients.Group($"trip:{tripId}")
.SendAsync("status_change", new
{
status, data, timestamp = DateTime.UtcNow
});
}
public override async Task OnDisconnectedAsync(Exception? ex)
=> await base.OnDisconnectedAsync(ex);
}
13. Payment Processing
Payment is one of the most critical and regulated components. Uber processes over $150 billion in annual gross bookings across 70+ currencies and multiple payment methods. The system must handle pre-authorizations, fare calculations with dynamic pricing, splits, tips, refunds, and driver payouts while maintaining PCI-DSS compliance.
Payment Flow
- Pre-Authorization: When rider requests a trip, we pre-authorize $1 on their payment method to validate it.
- Estimate Hold: Once matched, we place a hold for the estimated fare + 20% buffer.
- Capture: On trip completion, we capture the actual fare amount.
- Tip Processing: Tips are captured separately 24 hours after trip completion.
- Driver Payout: Driver earnings (fare minus platform commission) are batched and paid out daily.
| Payment Method | Pre-Auth | Capture Delay | Refund Window | Platform Fee |
|---|---|---|---|---|
| Credit/Debit Card | Yes | Instant | 30 days | 25% |
| Digital Wallet | Yes tokenized | Instant | 30 days | 25% |
| Cash | No | N/A | N/A | 20% |
| Corporate Account | Org-level | Weekly batch | 60 days | 20% |
14. Rating & Review System
Uber's bidirectional rating system creates accountability for both riders and drivers. After each trip, both parties rate each other from 1 to 5 stars. A driver below 4.6 may be deactivated. A rider below 4.2 may be deprioritized in matching.
Rating Calculation — Bayesian Average
Weighted Rating = (C x m + sum of ratings) / (C + n)
Where: C = confidence weight (30), m = global mean rating (4.6), n = number of ratings, sum of ratings = sum of all rating values.
Example: A new driver with 5 ratings averaging 5.0: (30 x 4.6 + 5 x 5.0) / (30 + 5) = (138 + 25) / 35 = 4.66 — pulled toward the mean.
15. Safety Features — SOS & Trip Sharing
Safety is Uber's highest priority after reliability. The platform includes multiple safety mechanisms that must work even under adverse conditions.
| Feature | Implementation | Latency Requirement |
|---|---|---|
| SOS Button | Immediate alert to safety team + local 911 via RapidSOS API | < 3 seconds to dispatch |
| Trip Sharing | Real-time trip link to trusted contacts via SMS/deeplink | Instant |
| Audio Recording | On-device encrypted audio during trip | Background |
| Speed Alerts | Driver notified if exceeding 120 km/h | < 1 second |
| Verify Driver | Rider must verify PIN or scan QR to start trip | Before trip start |
| Route Deviation Alert | ML detects significant route deviation | < 5 seconds detection |
| Trusted Contacts Auto-Share | Auto-shares trip during night hours 10pm-5am | On trip match |
SOS Flow: When rider presses SOS, the app immediately: (1) starts audio/video recording, (2) shares live location with Uber's 24/7 safety team, (3) connects rider to a safety agent via audio call, (4) if rider confirms emergency, dispatches local law enforcement via RapidSOS API. The entire flow from button press to law enforcement notification takes under 3 seconds.
16. Driver Onboarding & Background Check
Onboarding a new driver is a multi-step process that typically takes 3-7 business days. It involves document submission, identity verification, vehicle inspection, and background check.
Required Documents
- Valid driver's license (front + back photo)
- Vehicle registration
- Proof of insurance (rideshare endorsement)
- Profile photo (real-time selfie for liveness check)
- Social Security Number (US) for background check
- Vehicle inspection report (within last 12 months)
12. Ride Pooling — UberX Share
UberX Share is the most algorithmically complex feature. It matches multiple riders heading in similar directions into a shared vehicle, reducing costs for riders and increasing driver earnings per hour. The system must solve a real-time vehicle routing problem (VRP) variant with time windows.
Pool Matching Constraints
| Constraint | Value | Rationale |
|---|---|---|
| Max detour per rider | 2 minutes additional | Riders tolerate small delays for savings |
| Max pool size | 2 riders (4 in UberX Share) | Vehicle capacity and comfort |
| Route overlap threshold | 70% shared distance | Ensures meaningful pooling benefit |
| Price discount for pooling | 25-50% off UberX | Incentive to choose pool |
| Match window | 5 minutes after first rider request | Balance between wait time and match quality |
| Pickup sequencing | Nearest-first heuristic | Minimize total detour |
The pool matching algorithm uses an insertion heuristic: for each new pool ride request, it evaluates inserting the rider into existing active pool trips. For each candidate insertion, it calculates the additional detour cost (in seconds) for all existing riders and the new rider. If the maximum additional detour is within the 2-minute threshold and route overlap exceeds 70%, the insertion is accepted. The algorithm runs on a dedicated fleet of ML-optimized matching servers that can evaluate 10,000+ candidate insertions per second.
18. Notification System
The notification system handles billions of messages daily across multiple channels: push notifications (FCM/APNs), SMS (Twilio), email (SES), and in-app messaging. Notifications are triggered by trip lifecycle events, payment confirmations, safety alerts, and promotional campaigns.
Notification Architecture
| Channel | Latency Target | Use Cases | Provider |
|---|---|---|---|
| Push (Mobile) | < 2 seconds | Trip updates, driver arrival, payment receipt | FCM + APNs via SNS |
| SMS | < 10 seconds | OTP verification, safety alerts, trip sharing | Twilio / SNS |
| < 30 seconds | Weekly summaries, receipts, promotional | SES | |
| In-App | < 1 second | Chat with driver, trip status, support tickets | WebSocket |
Notifications are published to Kafka topics partitioned by user ID to guarantee ordering. A dedicated notification consumer service handles deduplication (using a Redis-backed dedup window of 5 minutes), rate limiting (max 10 push notifications per user per hour for non-critical), and channel fallback (if push fails, try SMS after 30 seconds).
19. Fraud Detection
Ride-sharing platforms face multiple fraud vectors: fake rides for driver earnings inflation, stolen payment credentials, promo code abuse, and account takeover. Uber's fraud detection system processes every ride through a real-time ML scoring pipeline.
Fraud Types and Mitigations
| Fraud Type | Detection Method | Action |
|---|---|---|
| Fake rides (GPS spoofing) | Anomaly detection: velocity checks, route plausibility, device fingerprint | Flag ride, hold payment, investigate |
| Stolen cards | Velocity checks, BIN matching, 3DS enforcement, device trust score | Block transaction, alert rider |
| Promo abuse | Device fingerprinting, IP clustering, account linkage graph | Revoke promo, ban accounts |
| Account takeover | Login anomaly (new device, new location, failed MFA) | Force re-authentication, lock account |
| Cash payment theft | Driver-rider collusion detection via frequent cash rides between same pair | Investigate, restrict cash option |
The fraud scoring model runs as a sidecar to the trip service, scoring each trip in real time with a fraud probability between 0 and 1. Trips scoring above 0.8 are automatically flagged, trips between 0.5-0.8 are queued for manual review, and trips below 0.5 pass through normally. The model is retrained weekly on labeled fraud data using XGBoost with 200+ features.
20. Database Sharding — Geo-Sharding
At Uber's scale, a single PostgreSQL instance cannot handle the write throughput or storage requirements. The solution is geographic sharding — partitioning data by the geographic region (city or metro area) where the trip occurred.
Sharding Strategy
Shard Key: City ID (derived from H3 level 3 cell of trip pickup location). All data for a city lives on the same shard, ensuring that queries within a city are served by a single database node without cross-shard joins.
Hot Shard Problem: Manhattan and downtown San Francisco generate 10x more trips than average. Solution: further shard hot cities by neighborhood (H3 level 5). The shard routing layer maintains a dynamic mapping table that can split hot shards without downtime.
Cross-Shard Queries: Rider trip history across cities and global analytics are served by a read replica that aggregates data from all shards via change data capture (CDC) through Kafka Connect. This eventual consistency (lag < 5 seconds) is acceptable for these use cases.
21. Caching Strategy
Caching is critical for reducing database load and meeting latency requirements. Uber uses a multi-tier caching strategy with different TTLs and invalidation patterns.
| Cache Key Pattern | Store | TTL | Invalidation |
|---|---|---|---|
| driver:{id}:location | Redis | 10 seconds | Write-through on GPS update |
| h3:{cell}:drivers | Redis | 5 seconds | Write-through on GPS update |
| surge:{h3Zone} | Redis | 5 minutes | Recalculated every 2 min |
| eta:{pickup_h3}:{dropoff_h3} | Redis | 60 seconds | TTL-based, refreshed on access |
| rider:{id}:profile | Redis | 1 hour | Invalidate on profile update |
| trip:{id}:status | Redis | 30 minutes | Write-through on state change |
| pricing:{city}:{ride_type} | Redis | 2 minutes | Recalculated every 2 min |
Cache-Aside for reads, Write-Through for driver locations: Driver location updates are written to Redis before being acknowledged to the driver app. This ensures that the matching service always reads the freshest location. For read-heavy data like rider profiles, we use cache-aside with lazy loading and LRU eviction.
Redis Cluster Topology: The Redis cluster is sharded by city hash, with 3 replicas per shard for read scaling. Each city's data lives in the same Redis shard as its database shard, minimizing network hops for geo-localized queries. Total Redis memory footprint: ~500 GB for all hot data globally.
22. Multi-Region Design
Uber operates across multiple cloud regions for low latency and disaster recovery. The primary regions are US-East, US-West, EU-West, and APAC-Southeast.
Multi-Region Strategy
| Data Type | Strategy | Consistency |
|---|---|---|
| User profiles | Global primary in US, async replication | Eventual (lag < 5s) |
| Trips | Geo-local primary (shard by city), no cross-region for writes | Strong within region |
| Driver locations | Region-local only, never replicated | N/A (ephemeral) |
| Payments | Global primary in US with regional read replicas | Strong for writes |
| Surge pricing | Region-local, computed independently | N/A |
| Analytics | S3 cross-region replication, Glue ETL jobs | Eventual (lag < 1 hour) |
Disaster Recovery: Each region can independently serve all ride requests within its geography. In a full region failure, the DNS-based traffic manager (Route53/Cloudflare) reroutes traffic to the nearest healthy region. Since driver locations are region-local, there is a brief period (~30 seconds) where matching in the affected cities pauses while drivers reconnect to the new region. Trip data is durable via cross-region CDC replication with RPO of < 1 second.
23. Cost Estimation
Estimating infrastructure cost for a 30M daily ride platform requires modeling compute, storage, networking, and third-party services.
| Component | Instance Type | Count | Monthly Cost |
|---|---|---|---|
| API Gateway / Load Balancer | Custom 8 vCPU, 16 GB | 20 | $8,000 |
| Microservices (12 services) | c5.2xlarge (8 vCPU) | 200 | $80,000 |
| WebSocket Gateway | c5.4xlarge (16 vCPU) | 60 | $36,000 |
| PostgreSQL (sharded, 50 shards) | r5.4xlarge (128 GB RAM) | 150 | $120,000 |
| Cassandra (location time-series) | i3.4xlarge (305 GB NVMe) | 200 | $140,000 |
| Redis Cluster | r5.xlarge (32 GB RAM) | 60 | $18,000 |
| Kafka Cluster | kafka.m5.2xlarge | 50 | $25,000 |
| S3 Storage (cold) | S3 Standard + Glacier | 500 TB | $12,000 |
| Data Transfer | Cross-AZ + Internet | — | $20,000 |
| Third-Party (Twilio, Stripe, Checkr) | — | — | $15,000 |
| ML/ETA Infrastructure | GPU instances (p3.2xlarge) | 30 | $45,000 |
| Monitoring (Datadog/Grafana) | — | — | $8,000 |
| Total Estimated Monthly | ~$527,000 |
Note: This is a simplified estimate. Uber's actual infrastructure spend is estimated at $1-2 billion annually. The estimate above focuses on the core ride-hailing platform and excludes UberEats, freight, autonomous vehicles, corporate overhead, and data science/ML training costs.
24. Interview Q&A — 10+ Questions
UPDATE trips SET status='completed', version=version+1 WHERE id=? AND version=?. If the version doesn't match, the operation is rejected (someone else already completed it). (3) Saga pattern with compensation — if payment capture fails after trip completion, the saga compensates by creating a payment retry task (not a trip rollback). (4) Distributed locks via Redis — for critical payment operations, acquire a Redis lock on trip_id with a 30-second TTL to prevent concurrent processing. (5) Reconciliation job — a daily batch job compares trip records with payment gateway records and flags discrepancies for manual review.25. Full C# Implementation (300+ Lines)
Below is a complete, production-grade C# implementation of the core Trip Service including the domain model, state machine, matching integration, pricing, and event publishing. This code demonstrates the key patterns used in building ride-sharing backends.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace RideSharing.Core.Trips
{
// ===== Domain Enums =====
public enum TripStatus
{
Requested, Matching, Matched, DriverEnroute,
Arrived, InProgress, Completed,
CancelledRider, CancelledDriver, NoDrivers
}
public enum RideType
{
UberX, UberXL, UberBlack, UberPool, UberShare
}
// ===== Domain Models =====
public class Trip
{
public Guid TripId { get; set; } = Guid.NewGuid();
public Guid RiderId { get; set; }
public Guid? DriverId { get; set; }
public TripStatus Status { get; set; } = TripStatus.Requested;
public RideType RideType { get; set; }
public double PickupLat { get; set; }
public double PickupLng { get; set; }
public string PickupAddress { get; set; } = string.Empty;
public double DropoffLat { get; set; }
public double DropoffLng { get; set; }
public string DropoffAddress { get; set; } = string.Empty;
public decimal EstimatedDistanceKm { get; set; }
public decimal ActualDistanceKm { get; set; }
public int EstimatedDurationSec { get; set; }
public int ActualDurationSec { get; set; }
public decimal BaseFare { get; set; }
public decimal SurgeMultiplier { get; set; } = 1.0m;
public decimal TotalFare { get; set; }
public string Currency { get; set; } = "USD";
public decimal PlatformFeePercent { get; set; } = 0.25m;
public string PaymentMethod { get; set; } = "card";
public string PaymentStatus { get; set; } = "pending";
public DateTime RequestedAt { get; set; } = DateTime.UtcNow;
public DateTime? MatchedAt { get; set; }
public DateTime? DriverEnrouteAt { get; set; }
public DateTime? ArrivedAt { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public DateTime? CancelledAt { get; set; }
public string? CancelReason { get; set; }
public string? CancelledBy { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public int Version { get; set; } = 1;
public decimal CalculateFare()
{
var distanceFare = EstimatedDistanceKm * 1.50m;
var timeFare = (EstimatedDurationSec / 60m) * 0.25m;
var baseFare = 2.50m + distanceFare + timeFare;
return Math.Round(baseFare * SurgeMultiplier, 2);
}
}
public class DriverMatchInfo
{
public Guid DriverId { get; set; }
public string FullName { get; set; } = string.Empty;
public double CurrentLat { get; set; }
public double CurrentLng { get; set; }
public int Heading { get; set; }
public int SpeedKmh { get; set; }
public decimal RatingAvg { get; set; }
public double AcceptanceRate { get; set; }
public double CancellationRate { get; set; }
public DateTime LastLocationUpdate { get; set; }
public string VehicleMake { get; set; } = string.Empty;
public string VehicleModel { get; set; } = string.Empty;
public string VehicleColor { get; set; } = string.Empty;
public string LicensePlate { get; set; } = string.Empty;
}
public class RideRequest
{
public Guid TripId { get; set; }
public Guid RiderId { get; set; }
public double PickupLat { get; set; }
public double PickupLng { get; set; }
public string PickupAddress { get; set; } = string.Empty;
public double DropoffLat { get; set; }
public double DropoffLng { get; set; }
public string DropoffAddress { get; set; } = string.Empty;
public RideType RideType { get; set; } = RideType.UberX;
public string PaymentMethodId { get; set; } = string.Empty;
}
public class MatchResult
{
public bool Success { get; set; }
public Guid? DriverId { get; set; }
public double MatchScore { get; set; }
public int EstimatedArrivalSec { get; set; }
public string? ErrorMessage { get; set; }
}
public class FareEstimate
{
public decimal MinFare { get; set; }
public decimal MaxFare { get; set; }
public decimal SurgeMultiplier { get; set; }
public int EstimatedDurationSec { get; set; }
public decimal EstimatedDistanceKm { get; set; }
public string Currency { get; set; } = "USD";
}
// ===== Configuration =====
public class TripServiceOptions
{
public int MatchingTimeoutSec { get; set; } = 120;
public int SearchHexRingSize { get; set; } = 2;
public int MaxSearchRadiusMeters { get; set; } = 5000;
public int MaxCancellationWindowSec { get; set; } = 30;
public decimal MaxFareSurchargePercent { get; set; } = 0.25m;
public int NoShowTimeoutSec { get; set; } = 300;
}
public class MatchWeights
{
public double Proximity { get; set; } = 0.40;
public double Rating { get; set; } = 0.20;
public double AcceptanceRate { get; set; } = 0.15;
public double CancellationRate { get; set; } = 0.15;
public double SurgeGap { get; set; } = 0.10;
}
// ===== Interfaces =====
public interface ITripRepository
{
Task<Trip?> GetByIdAsync(Guid tripId);
Task<Trip> SaveAsync(Trip trip);
Task<List<Trip>> GetRiderHistoryAsync(
Guid riderId, int page, int pageSize);
Task<bool> UpdateStatusOptimisticAsync(
Guid tripId, TripStatus expected,
TripStatus next, int expectedVersion);
}
public interface IDriverLocationCache
{
Task<List<DriverMatchInfo>> GetDriversInCellAsync(
string h3Cell, RideType rideType);
Task<DriverMatchInfo?> GetDriverAsync(Guid driverId);
Task<int> CountDriversInRadiusAsync(
double lat, double lng, int radiusMeters);
}
public interface IH3IndexService
{
string ToH3(double lat, double lng, int resolution);
List<string> GetDisk(string centerH3, int k);
}
public interface IPricingService
{
Task<decimal> GetSurgeMultiplierAsync(
string h3Zone, string cityId);
Task<FareEstimate> EstimateFareAsync(
double pickupLat, double pickupLng,
double dropoffLat, double dropoffLng,
RideType rideType, string cityId);
}
public interface IEventBus
{
Task PublishAsync<T>(string topic, T eventDto);
}
public interface INotificationService
{
Task NotifyRiderAsync(
Guid riderId, string title, string body);
Task NotifyDriverAsync(
Guid driverId, string title, string body);
}
// ===== Trip State Machine =====
public class TripStateMachine
{
private static readonly
Dictionary<TripStatus, HashSet<TripStatus>>
Transitions = new()
{
[TripStatus.Requested] = new()
{ TripStatus.Matching, TripStatus.CancelledRider },
[TripStatus.Matching] = new()
{ TripStatus.Matched, TripStatus.CancelledRider,
TripStatus.CancelledDriver,
TripStatus.NoDrivers },
[TripStatus.Matched] = new()
{ TripStatus.DriverEnroute, TripStatus.CancelledRider,
TripStatus.CancelledDriver },
[TripStatus.DriverEnroute] = new()
{ TripStatus.Arrived, TripStatus.CancelledRider,
TripStatus.CancelledDriver },
[TripStatus.Arrived] = new()
{ TripStatus.InProgress, TripStatus.CancelledRider },
[TripStatus.InProgress] = new()
{ TripStatus.Completed, TripStatus.CancelledDriver }
};
public bool CanTransition(
TripStatus from, TripStatus to)
=> Transitions.ContainsKey(from) &&
Transitions[from].Contains(to);
public IReadOnlyCollection<TripStatus>
GetAllowedTransitions(TripStatus from)
=> Transitions.ContainsKey(from)
? Transitions[from]
: new HashSet<TripStatus>();
}
// ===== Matching Service =====
public class MatchingService
{
private readonly IDriverLocationCache _cache;
private readonly IH3IndexService _h3;
private readonly ILogger<MatchingService> _logger;
private readonly MatchWeights _weights = new();
public MatchingService(
IDriverLocationCache cache,
IH3IndexService h3,
ILogger<MatchingService> logger)
{
_cache = cache;
_h3 = h3;
_logger = logger;
}
public async Task<MatchResult> FindBestDriverAsync(
RideRequest request,
int searchRingSize,
int maxCandidates,
CancellationToken ct)
{
var pickupH3 = _h3.ToH3(
request.PickupLat, request.PickupLng, 8);
var searchCells = _h3.GetDisk(
pickupH3, searchRingSize);
var allDrivers = new List<DriverMatchInfo>();
foreach (var cell in searchCells)
{
if (ct.IsCancellationRequested) break;
var drivers = await _cache
.GetDriversInCellAsync(cell, request.RideType);
allDrivers.AddRange(drivers);
}
var candidates = allDrivers
.Where(d =>
d.LastLocationUpdate >
DateTime.UtcNow.AddMinutes(-2))
.GroupBy(d => d.DriverId)
.Select(g => g.First())
.Take(maxCandidates)
.ToList();
if (!candidates.Any())
{
_logger.LogWarning(
"No candidates for H3={H3}", pickupH3);
return new MatchResult
{
Success = false,
ErrorMessage = "No drivers available"
};
}
var scored = candidates
.Select(d => new
{
Driver = d,
Score = Score(d, request.PickupLat,
request.PickupLng)
})
.OrderByDescending(x => x.Score)
.ToList();
var top = scored.First();
var etaSec = CalculateEtaSeconds(
top.Driver.CurrentLat,
top.Driver.CurrentLng,
request.PickupLat,
request.PickupLng);
_logger.LogInformation(
"Best match: Driver {Id}, Score {Score}, ETA {Eta}s",
top.Driver.DriverId, top.Score, etaSec);
return new MatchResult
{
Success = true,
DriverId = top.Driver.DriverId,
MatchScore = top.Score,
EstimatedArrivalSec = etaSec
};
}
private double Score(
DriverMatchInfo d, double pickupLat, double pickupLng)
{
var dist = Haversine(
d.CurrentLat, d.CurrentLng,
pickupLat, pickupLng);
var proximity = Math.Max(0,
1.0 - (dist / 5000.0));
var rating = (double)d.RatingAvg / 5.0;
var acceptance = d.AcceptanceRate;
var cancellation = 1.0 - d.CancellationRate;
var surgeGap = 1.0;
return Math.Round(
_weights.Proximity * proximity +
_weights.Rating * rating +
_weights.AcceptanceRate * acceptance +
_weights.CancellationRate * cancellation +
_weights.SurgeGap * surgeGap, 4);
}
private static int CalculateEtaSeconds(
double fromLat, double fromLng,
double toLat, double toLng)
{
var distMeters = Haversine(
fromLat, fromLng, toLat, toLng);
var avgSpeedMps = 8.0;
return (int)(distMeters / avgSpeedMps);
}
private static double Haversine(
double lat1, double lng1,
double lat2, double lng2)
{
const double R = 6371000;
var dLat = Rad(lat2 - lat1);
var dLng = Rad(lng2 - lng1);
var a = Math.Sin(dLat / 2) *
Math.Sin(dLat / 2) +
Math.Cos(Rad(lat1)) *
Math.Cos(Rad(lat2)) *
Math.Sin(dLng / 2) *
Math.Sin(dLng / 2);
return R * 2 * Math.Atan2(
Math.Sqrt(a), Math.Sqrt(1 - a));
}
private static double Rad(double deg)
=> deg * Math.PI / 180.0;
}
// ===== Main Trip Service =====
public class TripService
{
private readonly ITripRepository _repo;
private readonly MatchingService _matching;
private readonly IPricingService _pricing;
private readonly IEventBus _eventBus;
private readonly INotificationService _notif;
private readonly TripStateMachine _fsm;
private readonly TripServiceOptions _opts;
private readonly ILogger<TripService> _logger;
public TripService(
ITripRepository repo,
MatchingService matching,
IPricingService pricing,
IEventBus eventBus,
INotificationService notif,
IOptions<TripServiceOptions> opts,
ILogger<TripService> logger)
{
_repo = repo;
_matching = matching;
_pricing = pricing;
_eventBus = eventBus;
_notif = notif;
_opts = opts.Value;
_logger = logger;
_fsm = new TripStateMachine();
}
public async Task<Trip> RequestRideAsync(
RideRequest request, CancellationToken ct)
{
_logger.LogInformation(
"Rider {Rid} requesting ride from ({Plat},{Plng})",
request.RiderId,
request.PickupLat, request.PickupLng);
var estimate = await _pricing.EstimateFareAsync(
request.PickupLat, request.PickupLng,
request.DropoffLat, request.DropoffLng,
request.RideType, "default");
var trip = new Trip
{
TripId = request.TripId,
RiderId = request.RiderId,
RideType = request.RideType,
PickupLat = request.PickupLat,
PickupLng = request.PickupLng,
PickupAddress = request.PickupAddress,
DropoffLat = request.DropoffLat,
DropoffLng = request.DropoffLng,
DropoffAddress = request.DropoffAddress,
EstimatedDistanceKm = estimate.EstimatedDistanceKm,
EstimatedDurationSec = estimate.EstimatedDurationSec,
SurgeMultiplier = estimate.SurgeMultiplier,
BaseFare = estimate.MinFare,
Currency = estimate.Currency,
PaymentMethod = request.PaymentMethodId,
Status = TripStatus.Requested
};
await _repo.SaveAsync(trip);
await _eventBus.PublishAsync("trip.events", new
{
Type = "TripRequested",
TripId = trip.TripId,
RiderId = trip.RiderId,
Timestamp = DateTime.UtcNow
});
_ = Task.Run(async () =>
await AttemptMatchAsync(trip.TripId, ct), ct);
return trip;
}
private async Task AttemptMatchAsync(
Guid tripId, CancellationToken ct)
{
var trip = await _repo.GetByIdAsync(tripId);
if (trip == null) return;
if (!_fsm.CanTransition(
trip.Status, TripStatus.Matching))
return;
trip.Status = TripStatus.Matching;
await _repo.SaveAsync(trip);
using var cts = CancellationTokenSource
.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(
_opts.MatchingTimeoutSec));
try
{
var matchRequest = new RideRequest
{
TripId = trip.TripId,
RiderId = trip.RiderId,
PickupLat = trip.PickupLat,
PickupLng = trip.PickupLng,
RideType = trip.RideType
};
var result = await _matching
.FindBestDriverAsync(
matchRequest,
_opts.SearchHexRingSize,
maxCandidates: 20,
cts.Token);
if (result.Success &&
result.DriverId.HasValue)
{
trip.DriverId = result.DriverId;
trip.MatchedAt = DateTime.UtcNow;
trip.Status = TripStatus.Matched;
await _repo.SaveAsync(trip);
await _eventBus.PublishAsync(
"trip.events", new
{
Type = "TripMatched",
TripId = trip.TripId,
DriverId = result.DriverId,
MatchScore = result.MatchScore,
Timestamp = DateTime.UtcNow
});
await _notif.NotifyRiderAsync(
trip.RiderId,
"Driver Found!",
$"Your driver will arrive in " +
$"~{result.EstimatedArrivalSec / 60} min");
await _notif.NotifyDriverAsync(
result.DriverId!.Value,
"New Ride Requested",
$"Pickup at {trip.PickupAddress}");
}
else
{
trip.Status = TripStatus.NoDrivers;
trip.CancelledAt = DateTime.UtcNow;
await _repo.SaveAsync(trip);
await _notif.NotifyRiderAsync(
trip.RiderId,
"No Drivers Available",
"Please try again in a few minutes");
}
}
catch (OperationCanceledException)
{
trip.Status = TripStatus.NoDrivers;
trip.CancelReason = "Matching timeout";
await _repo.SaveAsync(trip);
await _notif.NotifyRiderAsync(
trip.RiderId,
"Matching Timed Out",
"No drivers responded. Please try again.");
}
}
public async Task<bool> CancelTripAsync(
Guid tripId, Guid userId, string reason)
{
var trip = await _repo.GetByIdAsync(tripId);
if (trip == null) return false;
var cancelStatus =
trip.RiderId == userId
? TripStatus.CancelledRider
: TripStatus.CancelledDriver;
if (!_fsm.CanTransition(
trip.Status, cancelStatus))
return false;
trip.Status = cancelStatus;
trip.CancelledAt = DateTime.UtcNow;
trip.CancelReason = reason;
trip.CancelledBy =
cancelStatus == TripStatus.CancelledRider
? "rider" : "driver";
await _repo.SaveAsync(trip);
await _eventBus.PublishAsync("trip.events", new
{
Type = "TripCancelled",
TripId = trip.TripId,
CancelledBy = trip.CancelledBy,
Reason = reason,
Timestamp = DateTime.UtcNow
});
if (trip.DriverId.HasValue)
await _notif.NotifyDriverAsync(
trip.DriverId.Value,
"Trip Cancelled",
"The rider has cancelled this trip.");
return true;
}
public async Task<bool> TransitionTripAsync(
Guid tripId, TripStatus newStatus,
string triggeredBy)
{
var trip = await _repo.GetByIdAsync(tripId);
if (trip == null) return false;
if (!_fsm.CanTransition(
trip.Status, newStatus))
{
_logger.LogWarning(
"Invalid transition {From} to {To}",
trip.Status, newStatus);
return false;
}
var prev = trip.Status;
trip.Status = newStatus;
trip.UpdatedAt = DateTime.UtcNow;
switch (newStatus)
{
case TripStatus.DriverEnroute:
trip.DriverEnrouteAt = DateTime.UtcNow;
break;
case TripStatus.Arrived:
trip.ArrivedAt = DateTime.UtcNow;
break;
case TripStatus.InProgress:
trip.StartedAt = DateTime.UtcNow;
break;
case TripStatus.Completed:
trip.CompletedAt = DateTime.UtcNow;
trip.TotalFare = trip.CalculateFare();
trip.PaymentStatus = "captured";
break;
}
await _repo.SaveAsync(trip);
await _eventBus.PublishAsync("trip.events", new
{
Type = "TripStateChanged",
TripId = trip.TripId,
PreviousStatus = prev.ToString(),
NewStatus = newStatus.ToString(),
TriggeredBy = triggeredBy,
Timestamp = DateTime.UtcNow
});
return true;
}
public async Task<Trip?> GetTripAsync(Guid tripId)
=> await _repo.GetByIdAsync(tripId);
public async Task<List<Trip>> GetRiderHistoryAsync(
Guid riderId, int page = 1, int pageSize = 20)
=> await _repo.GetRiderHistoryAsync(
riderId, page, pageSize);
}
}
26. Conclusion
Designing a ride-sharing platform at Uber's scale is one of the most challenging system design problems in modern software engineering. It requires deep expertise across real-time systems, geospatial data structures, distributed databases, machine learning, payment processing, and event-driven architecture. The key takeaways from this design are:
- Geospatial indexing with H3 is foundational — it transforms expensive radius queries into O(1) lookups, enabling sub-50ms driver matching.
- The matching algorithm must balance efficiency and fairness — pure nearest-driver is efficient but leads to driver attrition; the weighted scoring approach with fairness decay is Uber's proven solution.
- Dynamic pricing is a control system, not just a multiplier — hysteresis, cooldown periods, and bounds prevent the oscillation that would destroy user trust.
- Real-time tracking demands a purpose-built WebSocket infrastructure — stateful connections, consistent hashing, and graceful degradation under backpressure are non-negotiable.
- Geo-sharding solves the scale problem — partitioning data by city keeps queries local and avoids the performance cliff of global databases.
- Payment correctness is paramount — idempotency keys, optimistic locking, and reconciliation jobs ensure that money is never lost or double-charged.
- Safety features must work under adverse conditions — the SOS flow must complete in under 3 seconds even with degraded network connectivity.
This design supports 30M+ daily rides with 99.99% availability, sub-5-second matching latency, and real-time GPS tracking to millions of concurrent users. The C# implementation provides a production-ready foundation that can be extended with additional features like ride pooling, multi-city routing, and autonomous vehicle integration.
"The best system design is not the one that handles the happy path perfectly — it's the one that degrades gracefully when everything goes wrong." This is especially true for ride-sharing, where network partitions, GPS failures, and payment gateway outages are daily occurrences at scale.