Design a Ride-Sharing App (Ola / Uber)
1. Understanding the Problem
Design the Android rider-side client of a ride-sharing app like Ola or Uber. This means: entering a destination, getting a fare estimate, booking a ride, tracking the driver in real time on a map, and following the trip through to payment. The interviewer may also ask about the driver-side app β flag which side you're designing early.
The defining challenge is live driver position updates at low latency. The choice of transport protocol (WebSocket vs polling vs SSE) and how you animate the marker on the map without jank defines the entire architecture.
Learn This Pattern ββ Functional Requirements (Rider App)
- Enter pickup (auto-detected via GPS) and destination
- Show fare estimate and available ride types (Mini, Sedan, Auto)
- Book a ride; show driver matching progress
- Live driver location on map with ETA
- Trip state transitions: Driver en-route β Arrived β In-trip β Completed
- In-trip turn-by-turn route polyline on map
- Post-trip: receipt, rating, payment summary
βοΈ Non-Functional Requirements
- Location precision: GPS update every 3β5 s on driver side
- Map latency: Driver marker moves smoothly, <1 s lag
- Battery: Minimise GPS drain on rider side (coarser interval)
- Reconnection: WebSocket auto-reconnects on network switch
- Offline: Graceful degradation β last known driver position shown
- Background: Driver app posts location via Foreground Service
- Rider-side or driver-side (or both)? Start with rider.
- Should we design the matching algorithm server-side?
- Payment integration in scope?
- In-trip chat / SOS button required?
2. The Set Up
Trip State Machine
The entire app is driven by a TripState sealed class. Every screen and action is a function of the current state:
Core Components
3. High-Level Design
Transport Protocol: Why WebSocket?
| Option | Latency | Battery | Bi-directional | Best for |
|---|---|---|---|---|
| HTTP Polling (every 2s) | High (2s) | Bad | β | Simple dashboards |
| Long Polling | ~1s | Medium | β | Chat (legacy) |
| SSE (Server-Sent Events) | Low | Good | β | Push-only feeds |
| π WebSocket (OkHttp) | Very Low | Good | β | Live location tracking |
| gRPC streaming | Very Low | Good | β | Internal microservices |
WebSocket wins because the rider app also needs to send events to the server (cancel, chat message, SOS), making bi-directional communication necessary. SSE is receive-only.
4. Low-Level Design
Whiteboard: Full Rider Trip Pipeline
Flow 1: Booking & Driver Matching
| HomeFragment | TripViewModel | TripRepository | WS Manager | Server |
|---|---|---|---|---|
1User taps "Confirm Ride" β viewModel.bookRide(rideType) |
||||
2Sets state to TripState.SEARCHING β UI shows spinner |
||||
3POST /book β receives tripId |
||||
4Opens wss://api.app.com/trips/{tripId} |
||||
| 5WebSocket connected; auth header sent in handshake | ||||
| 6Server runs geo-spatial matching; finds nearest driver | ||||
7WS frame: {type:"DRIVER_FOUND", driver:{name, photo, plate, rating, lat, lng}} |
||||
8State β DRIVER_FOUND; emits driver info to UI |
||||
| 9Driver card animates up from bottom; map adds driver marker |
Flow 2: Live Driver Location (EN_ROUTE)
| Map (HomeFragment) | TripViewModel | WebSocket Manager | Driver App | Server |
|---|---|---|---|---|
| 1FusedLocation fires every 3s β Foreground Service | ||||
2Driver WS pushes {lat, lng, bearing} to server |
||||
3Server fans out LOCATION_UPDATE frame to rider WS |
||||
4Parses frame β updates driverLocation: StateFlow<LatLng> |
||||
5Fragment collects Flow β calls animateMarker(from, to, bearing) |
||||
| 6ValueAnimator interpolates lat/lng over 1 s; marker moves smoothly with rotation | ||||
7WS frame ETA_UPDATE β UI shows "Driver arrives in X min" |
Flow 3: Trip Completion & Receipt
| TripFragment | TripViewModel | WebSocket Manager | Room DB | Server |
|---|---|---|---|---|
| 1Driver marks trip complete β server triggers billing | ||||
2WS frame: {type:"TRIP_COMPLETED", fare, duration, distance, paymentMethod} |
||||
3State β COMPLETED; stores trip in Room |
||||
| 4INSERT trip_history (tripId, fare, route, timestamp) | ||||
| 5Close WebSocket cleanly (code 1000) | ||||
| 6Navigate to ReceiptFragment; show fare breakdown + rating prompt | ||||
7User submits rating β POST /rate in background coroutine |
Key Code: WebSocket Manager with Auto-Reconnect
class WebSocketManager @Inject constructor(private val client: OkHttpClient) {
private val _events = MutableSharedFlow<TripEvent>()
val events: SharedFlow<TripEvent> = _events
private var webSocket: WebSocket? = null
private var reconnectDelay = 1000L // start 1 s, max 30 s
fun connect(tripId: String, token: String) {
val req = Request.Builder()
.url("wss://api.rideapp.com/trips/$tripId")
.addHeader("Authorization", "Bearer $token")
.build()
webSocket = client.newWebSocket(req, buildListener(tripId, token))
}
private fun buildListener(tripId: String, token: String) = object : WebSocketListener() {
override fun onMessage(ws: WebSocket, text: String) {
val event = parseEvent(text) // sealed class TripEvent
_events.tryEmit(event)
reconnectDelay = 1000L // reset backoff on healthy message
}
override fun onFailure(ws: WebSocket, t: Throwable, r: Response?) {
// Exponential backoff reconnect
CoroutineScope(Dispatchers.IO).launch {
delay(reconnectDelay)
reconnectDelay = (reconnectDelay * 2).coerceAtMost(30_000L)
connect(tripId, token)
}
}
override fun onClosed(ws: WebSocket, code: Int, reason: String) {
if (code != 1000) connect(tripId, token) // abnormal close β reconnect
}
}
fun send(msg: String) = webSocket?.send(msg)
fun disconnect() { webSocket?.close(1000, "trip_complete") }
}
Key Code: Smooth Driver Marker Animation
// Interpolate marker position over 1 second for fluid movement
fun animateMarker(
marker: Marker,
from: LatLng, to: LatLng, bearing: Float
) {
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 1000
interpolator = LinearInterpolator()
addUpdateListener { anim ->
val t = anim.animatedValue as Float
val lat = from.latitude + t * (to.latitude - from.latitude)
val lng = from.longitude + t * (to.longitude - from.longitude)
marker.position = LatLng(lat, lng)
marker.rotation = bearing
}
}
animator.start()
}
// Camera follows driver smoothly (only when user hasn't manually panned)
private var userPanned = false
fun onDriverLocationUpdate(location: DriverLocation) {
animateMarker(driverMarker, currentLatLng, location.latLng, location.bearing)
currentLatLng = location.latLng
if (!userPanned) {
map.animateCamera(CameraUpdateFactory.newLatLng(location.latLng))
}
}
Key Code: Driver Foreground Service (Driver App)
// Driver app β continuous location upload even when app is backgrounded
class LocationForegroundService : Service() {
private val fusedClient by lazy {
LocationServices.getFusedLocationProviderClient(this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIF_ID, buildNotification("Trip in progress"))
val req = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 3000L)
.setMinUpdateIntervalMillis(2000L) // at most every 2 s
.build()
fusedClient.requestLocationUpdates(req, locationCallback, mainLooper)
return START_STICKY // system restarts if killed
}
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { loc ->
webSocketManager.send(
Json.encodeToString(LocationUpdate(loc.latitude, loc.longitude, loc.bearing))
)
}
}
}
override fun onDestroy() {
fusedClient.removeLocationUpdates(locationCallback)
super.onDestroy()
}
}
5. Potential Deep Dives
Battery Optimisation on Rider Side
The rider doesn't need GPS at full accuracy while waiting. Use a coarse interval (PRIORITY_BALANCED_POWER_ACCURACY, every 30 s) to keep the pickup pin updated. Only switch to PRIORITY_HIGH_ACCURACY when the driver is <500 m away (detected via WS ETA). This can cut GPS battery drain by 60% during the wait phase.
Handling Network Switch (4G β Wi-Fi)
When the network changes, OkHttp detects the connection drop and calls onFailure. The WebSocketManager's exponential backoff re-connects using the new interface. Because the server keeps trip state, the new WS session receives the current TripState and recent location in the first server frame β no state loss. Observe ConnectivityManager.registerDefaultNetworkCallback to proactively reconnect on network available.
Add random jitter to reconnect delays: delay = min(baseDelay * 2^attempt, maxDelay) + random(0..1000ms). Without jitter, thousands of clients that lost connection simultaneously all retry at the same moment β thundering herd. Jitter spreads the load.
Offline Graceful Degradation
If connectivity drops mid-trip: (1) the map freezes on the last known driver position, (2) a "Reconnectingβ¦" banner appears, (3) the UI stays in the current TripState. When reconnected, the WS resumes and sends the latest state β the client reconciles. If the trip completed while offline, the WS frame is delivered on reconnect and the app transitions to receipt screen.
ETA & Route Polyline
The server provides the route as an encoded polyline string (Google Polyline Algorithm). Decode it on the client with PolyUtil.decode(encodedPath) from the Maps SDK utility library. Draw it as a Polyline on the map. As the driver moves, remove segments the driver has passed by comparing driver LatLng to polyline points.
Surge Pricing Display
The /estimate response includes a surgeMultiplier (e.g., 1.8x) and a surgeExpiresAt timestamp. Show a countdown timer on the fare card: CountDownTimer(surgeExpiresAt - now). If the user waits past expiry without booking, re-fetch the estimate. This creates urgency without a dark pattern β the fare genuinely may change.
Trip History (Room)
Store completed trips in Room for offline access to receipts. The trip_history table stores: tripId, pickupAddress, destinationAddress, fare, duration, distance, driverName, rating, timestamp. Synced lazily β if the app was offline at completion, sync via a WorkManager job that calls GET /trips/{tripId} when online.
6. What is Expected at Each Level
- Knows FusedLocationProviderClient basics
- Can show a map with a marker
- Understands polling vs WebSocket at a high level
- Implements trip states as simple enum
- Fetches routes from API and draws polyline
- Handles basic permission flow for location
- Full WebSocket lifecycle with auto-reconnect + backoff
- Smooth marker animation with ValueAnimator + bearing
- State machine: sealed class + StateFlow in ViewModel
- Foreground Service for driver GPS
- Network change detection β WS reconnect
- Offline degradation with last known state
- Battery-adaptive location intervals
- Dead reckoning: predict driver position between WS frames
- Jitter in reconnect to avoid thundering herd
- Dual WebSocket (one for trip, one for chat/SOS)
- Location spoofing detection strategy
- Map tile caching for offline areas
- Client-side A/B testing of ETA display
- Frame-rate adaptive animation on low-end devices
7. Interview Questions
HTTP polling fires a request every N seconds regardless of whether the driver has moved. This wastes battery, generates unnecessary network traffic, and introduces latency equal to the poll interval. WebSocket establishes a persistent connection β the server pushes updates only when the driver's position changes. The rider gets sub-second location updates with less battery and bandwidth use. Additionally, WebSocket is bi-directional, allowing the rider to send cancel or chat messages on the same connection.
The server sends a location update every ~3 seconds. If the marker teleported to the new position, the user would see it jump. Instead, use a ValueAnimator that runs for 1 second and interpolates the marker's LatLng from the previous position to the new one β creating smooth motion. Set marker.rotation = bearing on each animation frame so the car icon points in the direction of travel. If a new update arrives before the animation completes, cancel the old animator and start a new one from the current interpolated position.
Android aggressively kills background processes to save battery. A Foreground Service keeps the app process alive by showing a persistent notification. For the driver app, this is mandatory: GPS must continue sending location to the server even when the driver switches to navigation or music apps. startForeground(notifId, notification) promotes the service, making it immune to background kill. The service must request ACCESS_BACKGROUND_LOCATION permission on Android 10+ and declare foregroundServiceType="location" in the manifest.
Use a sealed class with data per state. The ViewModel exposes a StateFlow<TripState>. The Fragment collects it and renders the correct UI for each state β no conditional spaghetti. Transitions are triggered by WebSocket events: DRIVER_FOUND moves from SEARCHING to DRIVER_FOUND. Illegal transitions (e.g., jumping from IDLE to IN_TRIP) are rejected β the state only advances via a reduce(currentState, event) function, making it predictable and testable.
The WebSocketManager's onFailure callback detects the drop. The reconnect logic uses exponential backoff with jitter: 1 s, 2 s, 4 s, ... capped at 30 s. Adding random jitter (Β±500 ms) prevents thundering herd if thousands of riders disconnect simultaneously. On reconnect, the server sends the current trip state and last known driver location in the first frame β the client reconciles and resumes smoothly. Meanwhile, the UI shows a "Reconnectingβ¦" overlay and the driver marker stays at its last position, freezing rather than disappearing.
Use FusedLocationProviderClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY) for a one-shot location (no continuous updates needed just for pickup). After requesting the necessary permissions (ACCESS_FINE_LOCATION), this returns the last known high-accuracy fix or acquires a fresh one. Reverse-geocode the coordinates to a human-readable address using Geocoder or the Maps Geocoding API. Show a draggable pin β moving the pin updates the address in real time via reverse-geocoding on drag end.
The booking API or a separate GET /route call returns an encoded polyline string. Decode it with PolyUtil.decode(encodedPolyline) (Maps SDK utility). Draw it with map.addPolyline(PolylineOptions().addAll(points).color(Color.BLUE).width(8f)). As the driver moves, clip completed segments: find the closest polyline point to the driver's current LatLng and remove all preceding points. Update the displayed polyline with the remaining points. This shows the remaining route getting shorter as the driver approaches.
The rider's GPS serves only two purposes: (1) detect initial pickup location (one-shot), (2) optionally track if the rider walks to a different pickup spot. For real-time tracking the driver's GPS is used, not the rider's. So: (1) After confirming the ride, stop continuous GPS updates on the rider side entirely. (2) Use PRIORITY_BALANCED_POWER_ACCURACY for the pre-booking pickup pin (network/Wi-Fi location, much cheaper than GPS). (3) Only the WS connection consumes power during the trip β OkHttp keeps it alive with periodic pings, which is far cheaper than GPS polling.
Cancel is a sensitive action β accidental taps cost money (cancellation fee after driver accepts). Implement a two-step confirmation: tap "Cancel" β bottom sheet with cancellation policy + "Confirm Cancel" button. On confirm: (1) POST /cancel {tripId, reason} via Retrofit; (2) Send a CANCEL frame on the WebSocket (faster server notification); (3) Handle the server response: if cancellation fee applies, show a confirmation dialog before committing. If offline, enqueue cancellation with WorkManager β but show the user that the cancellation is pending, not confirmed.
On launch, the app checks for an active trip: GET /trips/active. If one exists, it restores the last TripState, reconnects the WebSocket with the stored tripId (persisted in SharedPreferences or Room), and navigates directly to the TripFragment β skipping the home screen. The server sends the current state and recent driver location on WS connect, so the map and ETA resume correctly. FCM push notifications also help: if the app is killed and the trip completes, the rider receives a notification with a deep link to the receipt screen.
Rider app requires only ACCESS_FINE_LOCATION (foreground) for the pickup pin. Request it contextually when the user opens the app β not on first launch. Use the Activity Result API: registerForActivityResult(RequestPermission). If denied: show a rational explanation ("We need location to detect your pickup"), then re-request. If permanently denied: deep-link to Settings. Driver app additionally requires ACCESS_BACKGROUND_LOCATION (Android 10+), which must be requested separately and after the foreground permission. Show a clear UI explaining why background access is needed before showing the system dialog.
Use OkHttp's MockWebServer with WebSocket support. Create a fake WebSocketServer that enqueues messages. In tests: (1) Start MockWebServer; (2) Call wsManager.connect(tripId); (3) Have the server send test frames (DRIVER_FOUND, LOCATION_UPDATE); (4) Assert the events SharedFlow emitted the correct TripEvent values. For reconnect tests: close the WebSocket from the server side with a non-1000 code and assert the manager re-connects after backoff. Use TestCoroutineDispatcher to control time in backoff tests.
This is primarily a server concern, but the client should defend against it too: (1) Disable the "Book" button immediately on tap and set it to a loading state β prevent double-taps. (2) If the server returns a 409 Conflict (active trip already exists), navigate to the active trip screen rather than showing an error. (3) On app launch, always check GET /trips/active β if one exists from a previous session, restore it instead of showing the booking flow. The ViewModel holds the active tripId in a StateFlow; the repository rejects new booking attempts if tripId != null.
FusedLocationProviderClient (Google Play Services) fuses GPS, Wi-Fi, cell tower, and accelerometer data to provide the most accurate location using the least power. It shares the GPS hardware across apps β if Google Maps is already using GPS, FusedLocation can piggyback on that fix for free. It also automatically selects the best provider. Raw LocationManager requires manually choosing GPS vs Network provider, handling provider availability, and doesn't benefit from Play Services optimisations. The only case to use raw LocationManager is on devices without Play Services (e.g., Chinese OEM devices, LineageOS).
SOS requires guaranteed delivery: (1) Send a SOS frame on the existing WebSocket for immediate server notification; (2) Simultaneously call POST /sos {tripId, lat, lng} via Retrofit as a backup in case WS is down; (3) Share live location with emergency contacts via SMS using SmsManager (no permission needed for pre-saved contacts); (4) Optionally deep-link to the native dialler with the emergency number pre-filled. The SOS button should be a single large tap β no confirmation dialog. However, include a 5-second countdown with a cancel option to handle accidental taps.
Arrival detection is server-side: the server compares the driver's GPS coordinates to the pickup LatLng using a geo-fence (e.g., within 50 metres). When the driver enters the geo-fence, the server transitions the trip to DRIVER_ARRIVED and pushes a WS event. The client reacts by: (1) transitioning to the ARRIVED state; (2) showing "Your driver has arrived" notification (via NotificationManager); (3) starting a 5-minute waiting timer; (4) showing the "Start Trip" button on the driver's app. Never do geo-fence detection client-side β GPS can be spoofed.
Fare split is a social coordination problem: (1) The primary rider initiates a split request with a list of contact phone numbers and their share percentage; (2) Server sends each contact a deep link (SMS/WhatsApp) to accept the split; (3) Each accepting rider authenticates and links their payment method; (4) The primary rider sees a real-time status of who has accepted (WebSocket event or polling); (5) At trip completion, the server charges each rider's method for their share. On the client: the split screen is a separate Fragment reachable during SEARCHING or DRIVER_FOUND states. The primary rider can cancel any pending split before the trip starts.
Dead reckoning is predicting the current position from the last known position + speed + heading + elapsed time, without a new GPS fix. Between WS location frames (every 3 s), you can predict where the driver is: newLat = lat + speed * cos(bearing) * dt. This makes the marker appear to move continuously even without a new server update. It's useful for Staff+ answers but has risks: if the driver stops or turns, the prediction diverges until the next real update corrects it. A simpler approach (used in production) is the smooth interpolation with ValueAnimator over the update interval β visually equivalent without the physics.
Several layers: (1) Map tile caching: pre-cache the user's city map tiles using Google Maps' offline maps feature or Mapbox offline packs. (2) Trip data caching: once a ride is booked, cache the route polyline and driver info locally in Room β the user can still see the route even offline. (3) WS reconnect with backoff: aggressively reconnect but with jitter to avoid hammering a congested cell tower. (4) Graceful UI: show the last known driver position with a timestamp ("Last updated 30s ago") rather than hiding the map. (5) SMS fallback: show the driver's phone number so the rider can call directly if the app loses connectivity.
Location spoofing (fake GPS apps) is a real fraud vector in ride-sharing. Client-side mitigations: (1) Check Location.isFromMockProvider() (API 18) β reject locations with isMock = true; (2) On API 31+, check Location.isMock; (3) Enable SafetyNet / Play Integrity API attestation β devices running mock provider apps often fail the integrity check. Server-side (more robust): (1) Speed sanity checks β if the driver "moves" 5 km in 1 second, reject the location; (2) Cross-reference with telco cell tower location β if GPS says Mumbai but cell towers say Delhi, flag as suspicious; (3) Route adherence β if the driver deviates massively from the assigned route, alert the operations team.