Design Uber
System design interview solution for Design Uber. Includes requirements, API design, data model, architecture, scaling strategy, and tradeoffs.
Problem Statement
Design a system similar to Uber. The system should handle millions of users and provide a reliable, scalable experience.
Step 1: Clarifying Questions
Before diving into the design, ask these clarifying questions:
- What is the expected scale (users, requests per second)?
- What are the most critical features to support?
- What are the latency requirements?
- Do we need to support real-time features?
- What consistency guarantees are needed?
Step 2: Functional Requirements
- Core feature set for Uber
- User-facing APIs and interactions
- Data storage and retrieval
- Search and discovery (if applicable)
- Notifications (if applicable)
Step 3: Non-Functional Requirements
- Scalability: Handle millions of concurrent users
- Availability: 99.99% uptime (four nines)
- Latency: Sub-200ms for read operations
- Consistency: Eventually consistent where acceptable, strongly consistent for critical paths
- Durability: No data loss
Step 4: Back-of-the-Envelope Estimation
| Metric | Estimate |
|---|---|
| Daily Active Users | 10M |
| Read:Write Ratio | 10:1 |
| Average Request Size | 1 KB |
| Storage per year | ~10 TB |
| Peak QPS | 100K |
Step 5: API Design
POST /api/v1/resource
GET /api/v1/resource/{id}
PUT /api/v1/resource/{id}
DELETE /api/v1/resource/{id}
Step 6: Data Model
Define the core entities and their relationships. Consider the access patterns when choosing between SQL and NoSQL.
Step 7: High-Level Architecture
The system consists of these major components:
- Client Layer — Web/mobile clients
- API Gateway — Rate limiting, authentication, routing
- Application Servers — Business logic
- Database Layer — Primary storage
- Cache Layer — Redis/Memcached for hot data
- Message Queue — Async processing
Step 8: Detailed Component Design
Write Path
How data flows from client to persistent storage.
Read Path
How data is retrieved, including cache interactions.
Step 9: Scaling Strategy
- Horizontal scaling of application servers behind a load balancer
- Database sharding by user ID or geographic region
- Read replicas for read-heavy workloads
- CDN for static content delivery
- Auto-scaling based on traffic patterns
Step 10: Reliability and Fault Tolerance
- Data replication across availability zones
- Circuit breakers for dependent services
- Graceful degradation under high load
- Health checks and automated failover
Step 11: Monitoring and Observability
- Request latency (p50, p95, p99)
- Error rates by endpoint
- Database query performance
- Cache hit/miss ratios
- Queue depth and processing lag
Key Tradeoffs
| Decision | Option A | Option B | Chosen |
|---|---|---|---|
| Database | SQL | NoSQL | Depends on access patterns |
| Consistency | Strong | Eventual | Eventual for most reads |
| Communication | Sync | Async | Async for non-critical paths |
How to Present This in an Interview
- Start with clarifying questions (2 min)
- Define requirements (3 min)
- Do estimation (2 min)
- Design API and data model (5 min)
- Draw high-level architecture (10 min)
- Deep dive into critical components (10 min)
- Discuss tradeoffs and bottlenecks (5 min)
Practical Implementation for .NET Developers
In a .NET application, you would typically implement this pattern using the following approach:
ASP.NET Core setup: Create a service class that encapsulates the logic, register it with dependency injection, and inject it into your controllers or minimal API endpoints. The built-in DI container handles lifecycle management.
Entity Framework Core: For database interactions, EF Core provides the ORM layer. Use migrations for schema management and raw SQL for performance-critical queries. Consider Dapper for read-heavy paths where EF Core's overhead matters.
Azure integration: If deploying to Azure, leverage managed services — Azure Cache for Redis, Azure SQL, Azure Service Bus, Azure Cosmos DB. These eliminate operational overhead and provide built-in monitoring through Application Insights.
Testing: Use xUnit with Testcontainers for integration tests that spin up real databases in Docker. Mock external dependencies with NSubstitute. The WebApplicationFactory class lets you test your entire HTTP pipeline in-process.
Monitoring: Add Application Insights telemetry to track request latency, dependency calls, and custom metrics. Use structured logging with Serilog to make production debugging possible:
Log.Information("Processing order {OrderId} for {CustomerId}", orderId, customerId);
This gives you searchable, structured logs in Azure Monitor or Seq.
Deep-Dive: Clarifying Questions for Uber
- What geographic coverage? Global (900+ cities) or a single city? Global requires geo-distributed architecture with regional data residency.
- How do we match riders to drivers? Nearest available driver? Best ETA? Consider traffic, driver rating, surge pricing? The matching algorithm is the core intellectual property.
- What is the location update frequency? Drivers send GPS updates every 3-4 seconds. With 5 million active drivers, that is 1.25-1.67 million location updates per second.
- Do we need surge pricing? Dynamic pricing based on supply/demand ratio requires real-time market-making computation.
- What geospatial indexing do we use? Google S2, Uber H3, or traditional geohashing? This choice affects query efficiency and precision.
- How do we handle real-time tracking? Riders need to see their driver approaching in real-time. This requires pushing location updates to specific rider apps via WebSocket connections.
Specific Functional Requirements
- Ride Request: Rider specifies pickup and dropoff locations, selects ride type (UberX, XL, Black), sees price estimate
- Driver Matching: System matches the rider with the best available driver based on ETA, proximity, and other factors
- Real-Time Tracking: Both rider and driver see each other's location updating in real-time during the ride
- ETA Estimation: Predict pickup time and trip duration using historical traffic data, real-time conditions, and road graphs
- Surge Pricing: Dynamically adjust prices based on local supply (available drivers) and demand (ride requests) ratios
- Payment Processing: Automatically charge the rider after trip completion with fare breakdown
- Rating System: Both riders and drivers rate each other after each trip
Specific API Endpoints
POST /api/v1/rides/estimate
Body: { "pickup": { "lat": 37.7749, "lng": -122.4194 }, "dropoff": { "lat": 37.3382, "lng": -121.8863 }, "ride_type": "uberx" }
Response: { "estimated_fare": "$23-28", "estimated_pickup": "4 min", "estimated_trip": "42 min", "surge_multiplier": 1.0 }
POST /api/v1/rides/request
Body: { "pickup": {...}, "dropoff": {...}, "ride_type": "uberx", "payment_method_id": "pm_abc" }
Response: { "ride_id": "ride_123", "status": "matching", "driver": null }
WebSocket /ws/rides/:ride_id/track
Messages: { "type": "location_update", "lat": 37.77, "lng": -122.41, "heading": 180, "eta_seconds": 120 }
PUT /api/v1/drivers/location
Body: { "lat": 37.7749, "lng": -122.4194, "heading": 90, "speed": 35 }
(Called every 3-4 seconds by driver app)
Specific Data Model
Driver Location Store (Real-time): Use an in-memory geospatial index. Uber uses H3 hexagonal grid cells: driver locations are indexed by H3 cell ID (resolution 7, approximately 5 km2 hexagons). When a ride request comes in, search the pickup location's H3 cell and adjacent cells for available drivers.
| Store | Key | Value |
|---|---|---|
| Driver Locations (Redis) | h3_cell_id | Set of (driver_id, lat, lng, status, last_updated) |
| Active Rides (PostgreSQL) | ride_id | rider_id, driver_id, pickup, dropoff, status, fare, timestamps |
| Driver Profiles | driver_id | name, vehicle, rating, documents, status |
| Trip History | trip_id | All trip details, route taken, fare breakdown |
Geospatial Indexing with H3: Uber created the H3 library for hexagonal hierarchical geospatial indexing. Hexagons tile the Earth's surface uniformly (unlike rectangular grid cells), and each hexagon has 6 equidistant neighbors. This makes proximity searches efficient: find all drivers in the target cell and its 6 neighbors (7 lookups total, regardless of geographic area).
Specific Back-of-the-Envelope Numbers
Traffic:
- 20 million rides per day globally (Uber's reported number)
- 5 million active drivers sending location updates every 4 seconds = 1.25 million location updates/second
- Ride matching: 20M rides/day = ~230 ride requests/second average, peaking at 3-5x = ~1,000/second
Storage:
- Driver location updates: 1.25M/sec * 50 bytes = 62.5 MB/second = 5.4 TB/day of raw location data
- Active driver locations (in-memory): 5M drivers * 100 bytes = 500 MB (fits in RAM easily)
- Trip history: 20M trips/day * 2 KB = 40 GB/day = ~14 TB/year
Matching latency target:
- Under 10 seconds from ride request to driver assignment
- Under 3 seconds for the geospatial search component
- Driver location must be under 4 seconds stale
Map/routing:
- ETA calculation uses precomputed road graphs with real-time traffic overlays
- Route calculation: Dijkstra or A* on road graph, under 100ms per route