Blog

Android System Design: Design Uber's Mobile App

Blog
Android System Design: Design Uber's Mobile App
22 min
·
November 18, 2025
·
Android Architect
·
hard

Android System Design: Design Uber's Mobile App

A complete Android system design guide for designing Uber's mobile app — written in a conversational interview style. Covers Clean Architecture, real-time location with FusedLocationProviderClient and Foreground Service, gRPC vs WebSocket for driver tracking, map marker smoothing, trip state machine, background location permissions across Android versions, offline handling, and battery optimisation — for senior Android engineer interviews.

Android System Design: Design Uber's Mobile App

Designing Uber's Android app is one of the richest mobile system design questions you can get in an interview. It shows up at Uber obviously, but also at Lyft, DoorDash, Google, Snap, and any company building location-aware or real-time experiences.

What makes it interesting isn't the ride-hailing domain. It's the Android platform challenges hiding inside it. How do you stream a driver's location to the rider without killing the battery? How does a Foreground Service survive being backgrounded during an active trip? What does gRPC buy you over WebSockets for bidirectional location streaming? How do you animate a car marker smoothly across a map when location updates arrive every few seconds?

These are the questions interviewers are really asking. This guide covers all of them — with the conversational back-and-forth you'd actually have in the room.


Step 1: Clarify the Scope

Interviewer: Let's start. Design the Uber Android app.

Candidate: Before I jump in — a few quick questions. Are we designing the rider app, the driver app, or both? They have meaningfully different requirements. What flows are in scope — just the core ride-requesting and tracking experience, or also payment, ratings, trip history? And what's the offline expectation — if the network drops mid-trip, should the user still see their last known state?

Interviewer: Good. Focus on the rider app for now but make sure your location architecture applies to the driver side too. Core flows only — request a ride, track the driver, complete the trip. Assume the user might lose network briefly during the trip.

Candidate: Perfect. I'll design the rider-facing experience with a location layer that extends naturally to the driver app. Let me start with requirements and then walk through the architecture.


Requirements

Functional

  • Show nearby available drivers on a map in real time
  • Allow a rider to request a ride and receive a driver assignment
  • Track the assigned driver's location in real time during the trip
  • Show ETA, route, and trip status throughout the journey
  • Handle payment and receipt on trip completion
  • Push notifications for driver assignment, driver arrival, and trip completion

Non-Functional

  • Real-time location — driver position updates must feel smooth, not jumpy
  • Battery efficiency — continuous GPS and network use must be minimised without sacrificing UX
  • Offline resilience — the current trip state must survive network drops
  • Foreground reliability — location tracking must not be killed by the OS during an active trip
  • Permission compliance — must handle Android's evolving location permission model correctly

Back-of-the-Envelope Estimates

Interviewer: Before you get into the design — what kind of scale are we talking about on the client side?

Candidate: On the client specifically, the numbers look like this:

plaintext
Concurrent active trips globally: ~5 million (peak)
Location update frequency (driver): every 3–5 seconds
Payload per update: ~100 bytes (lat, lng, heading, speed, timestamp)
 
Driver → server upload: ~20–30 KB/minute
Server → rider download: same, per active trip

What those numbers tell you is that bandwidth isn't the problem — 100 bytes every 4 seconds is nothing. The real constraint is battery. Holding a GPS lock and a persistent network connection for a 30-minute trip is meaningful battery work. That shapes every architecture decision we'll make.


Client Architecture

Interviewer: Walk me through the overall architecture before we go deep.

Candidate: Clean Architecture with MVVM at the presentation layer:

plaintext
UI Layer
  Composables / Fragments  ←  ViewModel (StateFlow)
 
Domain Layer
  Use Cases: RequestRide, TrackDriver, UpdateTripStatus
 
Data Layer
  RideRepository
    ├── Remote: gRPC / Retrofit
    └── Local: Room (trip state, last known location)
 
Location Layer (cross-cutting)
  LocationManager (wraps FusedLocationProviderClient)
  TripTrackingService (Foreground Service — active during a trip)

The most important structural decision is that Room is the single source of truth for trip state. The UI always observes Room via Flow. It never reads directly from the network. When a location update arrives over gRPC, it gets written to Room, which triggers the UI update. That means if the process is killed mid-trip and the user relaunches, Room has the last known state and the app restores exactly where they were — no blank screen, no defaulting to home.


Location Permissions: The Android Reality

What the permission model looks like: Android has three tiers of location access, and which one you need depends on your feature.

Foreground location (ACCESS_FINE_LOCATION) — available when the app is visible or a Foreground Service is running. Enough for the rider app.

While-in-use — the default on Android 10+. Location only when the app is in the foreground. Fine for the rider; not enough for the driver during a trip.

Background location (ACCESS_BACKGROUND_LOCATION) — location when the app is not visible at all. Required for the driver app.

Interviewer: Does the rider app need background location?

Candidate: No. The rider is actively watching the tracking screen during a trip. Foreground location is sufficient. What we do need is a Foreground Service — which keeps the process alive and allows location access even if the rider briefly switches apps. The distinction matters: a Foreground Service with ACCESS_FINE_LOCATION is different from requesting background location. We don't need the latter.

Interviewer: What about the driver app?

Candidate: The driver app is a different story. The driver needs location access constantly — while taking calls, checking messages, even with the screen off. That requires ACCESS_BACKGROUND_LOCATION. And this is where Android versioning gets important.

On Android 10, you could request background location alongside the standard dialog. On Android 11, that changed — the system dialog no longer offers "Allow all the time." The user must go to Settings themselves. On Android 14, accessing precise location from a background context requires the user to explicitly grant ACCESS_BACKGROUND_LOCATION, or the system throws a SecurityException. So the driver app must check for this permission before starting the Foreground Service, handle the denial gracefully, and guide the user through the Settings path — not just show a dialog and hope.

Most candidates say "we need location permission" and move on. Calling out the three tiers and the Android 10, 11, and 14 changes is what distinguishes a senior answer.

This is one of the clearest signals of genuine Android platform experience vs surface-level prep — and interviewers at Uber and DoorDash know exactly what to listen for. Getting the three tiers and version-specific changes crisp before your interview is worth specific time. Mockingly.ai includes Android location permission follow-ups in its Uber and ride-hailing simulations.


FusedLocationProviderClient: The Right Location API

What it is: FusedLocationProviderClient is Google Play Services' intelligent location API. It fuses GPS, Wi-Fi, cell tower, and sensor data to produce the best possible fix at the lowest possible battery cost. It's the officially recommended location API — not LocationManager directly.

Interviewer: Why not use the raw LocationManager?

Candidate: Two reasons. First, LocationManager doesn't do sensor fusion — you're locked into one source. If GPS isn't available, you don't automatically fall back to network-based location. Second, raw GPS is power-hungry. It keeps the GPS chip active continuously. FusedLocationProviderClient is intelligent about this — if you're stationary, it reduces the fix rate automatically. If the device already has a recent location from another app, it reuses it rather than firing the GPS chip again.

Interviewer: How do you configure it for Uber's use case?

Candidate: The LocationRequest parameters vary by trip state. During an active trip:

plaintext
interval:             4000ms    (preferred update interval)
fastestInterval:      2000ms    (maximum rate — never exceed this)
priority:             PRIORITY_HIGH_ACCURACY
smallestDisplacement: 5 metres  (skip update if we haven't moved 5m)

The smallestDisplacement of 5 metres is a battery win. If the driver is stopped at a red light, we don't need GPS updates every 4 seconds — nothing has changed. The filter is essentially free and eliminates a meaningful chunk of idle updates.

Pre-trip — when the rider is on the home screen browsing nearby drivers — I'd switch to PRIORITY_BALANCED_POWER_ACCURACY. Network-based location, less battery. The rider doesn't need GPS-quality precision to see that drivers are roughly in their neighbourhood. Switch to HIGH_ACCURACY only when a trip becomes active.


Foreground Service: Keeping Location Alive During a Trip

What it is: a Foreground Service is an Android service that the user is aware of via a persistent notification. The OS will not kill it for memory reclamation under normal conditions. It's the mechanism that keeps work running while the user isn't actively looking at the app.

Interviewer: Why does Uber need a Foreground Service? Can't you just keep the location running in the background?

Candidate: From Android 8 onwards, background services are restricted. The OS will kill a background service after a few minutes of the app not being in the foreground. For a 30-minute trip where the rider might switch apps to reply to a message, that's a problem — location tracking would silently stop.

A Foreground Service gets a higher process priority. The OS won't kill it for memory reasons. The trade-off is the persistent notification — you have to show the user that something is running. For Uber, that's "Trip in progress" in the notification shade, which is actually correct UX — the user should know the app is actively tracking.

Interviewer: What does the service look like?

Candidate: When the trip state transitions to DRIVER_ASSIGNED — that's when the Foreground Service starts. It runs through DRIVER_EN_ROUTE, DRIVER_ARRIVED, TRIP_IN_PROGRESS, and stops on TRIP_COMPLETED or TRIP_CANCELLED. Inside the service:

plaintext
TripTrackingService (Foreground Service)
  → starts persistent notification: "Uber trip in progress"
  → requests location updates via FusedLocationProviderClient
  → for the rider: receives driver location via gRPC stream, writes to Room
  → for the driver: sends own location to server every 4 seconds via gRPC
  → stops itself when trip ends

There are two manifest attributes I'd call out specifically. First, android:foregroundServiceType="location" — this is required from Android 10. Without it, the service will not receive location updates when targeting API 29+. Second, android:stopWithTask="false" — this means the service keeps running even if the user swipes the app off the recents screen. Without it, a rider who accidentally swipes the app loses their tracking screen entirely.


Real-Time Communication: gRPC vs WebSocket

What they are: WebSockets provide a persistent, full-duplex TCP connection where either side can send at any time. gRPC is Google's RPC framework built on HTTP/2, using Protocol Buffers for serialisation, with support for bidirectional streaming.

Interviewer: Which one would you use for location streaming, and why?

Candidate: gRPC, for four reasons.

First, serialisation efficiency. A location update in Protobuf is 15–20 bytes. The same object in JSON over a WebSocket is 80–120 bytes. Multiplied across millions of location updates per second system-wide, that's a material difference. On the Android client specifically, smaller payloads mean the radio finishes its transmission faster and can drop back to a lower power state sooner.

Second, HTTP/2 multiplexing. Multiple gRPC streams share a single TCP connection. The driver's outgoing location stream and the rider's incoming trip status stream coexist on one connection without separate socket overhead.

Third, bidirectional streaming. A single DriverTracking gRPC call lets the driver upload location and simultaneously receive server-side events — route updates, trip status changes — back down the same connection.

Fourth, built-in flow control. HTTP/2 handles backpressure natively. If the server is slow, it signals the client to slow down rather than having the client flood the connection.

Interviewer: How does gRPC integrate with the rest of the Android stack?

Candidate: Cleanly, actually. gRPC streaming maps naturally to Kotlin Coroutines. Incoming server messages become a Flow that the ViewModel collects. The FusedLocationProviderClient also has a locationFlow() extension function. So the driver app's upload pipeline becomes:

plaintext
FusedLocationProviderClient.locationFlow()
    .map { location → location.toProto() }
    .collect { proto → grpcStream.send(proto) }

The whole pipeline is coroutine-native — no callbacks, no manual threading.

Interviewer: When would you use WebSockets instead?

Candidate: If the team is allergic to adding the gRPC dependency — it's heavier than OkHttp. Or if the existing infrastructure is WebSocket-based and the migration cost isn't justified. In that case, WebSocket with binary MessagePack serialisation gets you most of the efficiency benefit. I'd name it as a viable alternative with those caveats, not as an equivalent choice.

The gRPC vs WebSocket question is one where interviewers expect you to argue from first principles — serialisation efficiency, HTTP/2 multiplexing, flow control — not just say "gRPC is better." If that reasoning doesn't flow naturally yet, Mockingly.ai has Android real-time architecture simulations where this comparison is a standard probe.


Map Rendering and Marker Smoothing

Interviewer: How do you make the car marker move smoothly? In basic implementations it teleports between positions.

Candidate: Right — and that looks terrible. GPS updates arrive every 3–5 seconds. If you just call marker.position = newLatLng on each update, the car jumps. The solution is to animate the marker between its current position and the new position over the update interval.

Use a ValueAnimator that runs for the expected update interval — say 4 seconds. On each animation frame, interpolate the marker position between old and new coordinates. For geographic interpolation, use SphericalUtil.interpolate() from the Google Maps Android Utilities library rather than linear interpolation on raw lat/lng values. Linear interpolation on raw coordinates is inaccurate at anything beyond very short distances because the Earth isn't flat.

Simultaneously animate the bearing — the rotation of the car icon to match the driver's heading. GPS provides a bearing value. Interpolate it alongside the position. The result is a car that visually turns as it moves, not one that snaps to a new angle.

Interviewer: What happens if a location update is delayed — say there's a network hiccup and the next update is 8 seconds late?

Candidate: You need a dead reckoning timeout. If no new update arrives within 2× the expected interval, stop animating. Keep the marker at the last confirmed position. Don't extrapolate — you don't know if the driver stopped, turned around, or just had a bad connection. When the next update arrives, resume the animation from wherever you stopped. The rider sees the car briefly pause, then continue — which is honest.

Interviewer: And if there are 30 drivers visible on the map simultaneously?

Candidate: Batch the updates. Don't redraw the map on every individual location message. Collect all incoming location updates within a 500ms window and apply them together in one render pass. Thirty individual map redraws per second will choke the UI thread. One batched redraw every 500ms at the map's native refresh rate is invisible to the user and much kinder to the CPU.


Trip State Machine

Interviewer: How do you model the trip lifecycle on the client?

Candidate: As an explicit state machine. This is one of the things that distinguishes a senior answer. The trip has well-defined states and transitions, and modelling them explicitly prevents the scattered if-else chains that make lifecycle code fragile.

plaintext
IDLE
  │  Rider taps "Request Ride"

REQUESTING
  │  Server assigns a driver

DRIVER_ASSIGNED
  │  Driver starts driving to pickup

DRIVER_EN_ROUTE
  │  Driver reaches pickup

DRIVER_ARRIVED
  │  Rider boards, driver starts trip

TRIP_IN_PROGRESS
  │  Driver reaches destination

TRIP_COMPLETED


RATING_PENDING  →  IDLE

Plus failure states: any state can transition to CANCELLED or ERROR.

In Kotlin, this is a sealed class:

kotlin
sealed class TripState {
    object Idle : TripState()
    object Requesting : TripState()
    data class DriverAssigned(val driver: DriverInfo, val eta: Int) : TripState()
    data class DriverEnRoute(val driver: DriverInfo, val location: LatLng) : TripState()
    data class DriverArrived(val driver: DriverInfo) : TripState()
    data class TripInProgress(val driver: DriverInfo, val route: List<LatLng>) : TripState()
    data class TripCompleted(val receipt: Receipt) : TripState()
    data class Error(val reason: String) : TripState()
}

The ViewModel exposes this as a StateFlow<TripState>. The UI observes it and renders the correct screen for each state. The Foreground Service starts when the state becomes DRIVER_ASSIGNED and stops on TRIP_COMPLETED. Everything is driven from one observable source.

Interviewer: What happens if the process is killed while a trip is in progress?

Candidate: Room has the current TripState — it's written on every transition. When the app relaunches, the ViewModel reads from Room and restores to the correct state. But Room only has the last state before the kill. The trip might have progressed while the phone was offline. So on app start, we also make a GET /trips/current REST call to reconcile with the server's authoritative state. The gRPC stream then takes over for real-time updates from that point. The user lands on the tracking screen mid-trip, not the home screen.

The process-kill recovery question — "what happens if the process is killed mid-trip?" — is the single most common deep-dive in Android system design interviews at Uber and DoorDash. The Room-first + REST-reconciliation answer covers it completely. Getting that explanation out clearly, without hesitation, is what Mockingly.ai helps you practise.


Real-Time Driver Location: The Rider's View

Interviewer: Walk me through the full flow of a location update from the driver's phone to the rider's map.

Candidate: End to end it looks like this:

plaintext
[Driver's phone]
  FusedLocationProviderClient → TripTrackingService
    → gRPC stream → sends DriverLocation to server every 4s
 
[Uber Server]
  Receives driver location
  Pushes to rider's active gRPC connection
 
[Rider's phone]
  TripTrackingService receives DriverLocation
  → writes DriverLocationEntity to Room
  → MapViewModel observes Room Flow<DriverLocation>
  → animates car marker to new position

Room is the buffer in the middle. If the rider's UI is briefly paused — notification shade pulled down, incoming call — updates accumulate in Room. When the UI resumes, the map catches up from the buffered positions rather than showing a stale state or waiting for the next network push.


Offline Handling and Network Resilience

Interviewer: The rider goes through a tunnel mid-trip. What happens?

Candidate: The gRPC stream drops. The car marker stops updating — it holds at the last known position. We show a subtle "Reconnecting..." indicator, not a full-screen error. Nothing dramatic. When the network returns, the ConnectivityManager.NetworkCallback fires and we immediately re-establish the gRPC stream. The marker animation resumes from wherever it left off.

Interviewer: What if the rider taps "Request Ride" and the network drops before the server responds?

Candidate: Without handling, the client might retry and submit two ride requests. To prevent that, the client generates a clientRequestId UUID before sending the request. This ID is sent with the request and stored locally. If the network drops and the client retries, it uses the same clientRequestId. The server de-duplicates on this ID — if it's already seen it, it returns the original response rather than creating a new trip. This is standard idempotency handling and it prevents the user from accidentally booking two Ubers.


Push Notifications: FCM for Killed-State Events

Interviewer: How do you handle events when the app is completely killed?

Candidate: FCM data messages. Not notification messages — data messages.

With notification messages, the system renders the notification before our code runs. We lose the ability to suppress it if the user is already in the app, and we can't update Room silently. With data messages, our FirebaseMessagingService gets called first. We write to Room, then decide whether to show an OS notification.

Key events that arrive via FCM when the app is killed: driver assigned, driver arrived, and trip cancelled by driver. Each one carries enough data to update the TripState in Room. When the user taps the notification, the app opens, Room has the updated state, and the ViewModel navigates to the correct screen directly — no extra server call needed for the initial render.

If the user is already on the tracking screen when one of these events arrives, the ordered broadcast pattern suppresses the OS notification and the UI updates inline via the Room → Flow → ViewModel pipeline. No toast, no dialog — the screen just transitions to the new state.


Battery Optimisation

Interviewer: This is a lot of GPS and network work for a 30-minute trip. How do you keep battery drain reasonable?

Candidate: A few things working together.

First, adaptive location accuracy by trip state. On the home screen: PRIORITY_BALANCED_POWER_ACCURACY with 15-second intervals — network-based location, minimal GPS. During DRIVER_EN_ROUTE when the driver is far away: HIGH_ACCURACY but with an 8-second interval and a 10-metre displacement filter. During TRIP_IN_PROGRESS: 4-second interval, 5-metre filter. The GPS chip works hardest only when the user actually needs high precision.

Second, server-side adaptive frequency. The server can tell the driver app to slow down its location uploads when the driver is far from the destination. No need for 3-second updates when the driver is 15 minutes away. This reduces radio usage on the driver's phone, which is the one that matters most for battery.

Third, gRPC's persistent connection. Every avoided connection setup is a battery saving. The HTTP/2 connection stays open and amortises the setup cost across thousands of location messages.

Fourth, no explicit WakeLock. The Foreground Service keeps the CPU active when the system needs it to. Explicit wakelocks are a battery antipattern — they're often held longer than intended and prevent the CPU from entering deep sleep states between location updates.

Interviewer: What about Doze mode?

Candidate: During an active trip, the rider is looking at the screen — Doze won't engage. But if the screen turns off during a long wait (say the driver is 20 minutes away), Doze could activate. The Foreground Service is explicitly exempt from Doze restrictions. It will keep running and receiving updates. WorkManager, by contrast, would be deferred to a maintenance window — which is why you use a Foreground Service for this and not WorkManager.


Map Performance

Interviewer: Any map-specific performance concerns?

Candidate: A few worth naming.

Avoid animating outside the viewport. If the camera is zoomed way out and the driver marker is a single pixel, skip the bearing animation entirely. The computation is wasted and the user can't see it anyway.

Prefetch map tiles for the expected route. When the trip enters DRIVER_ASSIGNED, the app knows both the driver's starting position and the rider's pickup point. Pre-request the map tiles along that corridor. By the time the driver is actually driving that route, the tiles are cached on-device and the map renders instantly as the camera moves. No white tile squares mid-trip.

Trim the route polyline progressively. As the trip moves, the portion of the polyline already driven is removed. Keep only the remaining route visible. This is a LatLng list update on the existing polyline object — not a redraw of the entire polyline from scratch.


Common Interview Follow-ups

"The marker smoothing looks good, but at very high zoom levels the car seems to jitter. Why?"

At zoom level 18 or 19, GPS noise becomes visible. A stationary or slow-moving device can report positions that vary by 2–5 metres randomly. SphericalUtil.interpolate() faithfully follows those jittery positions. The fix is to apply a Kalman filter — or a simpler exponential moving average — to incoming locations before feeding them to the animator. This smooths out noise while preserving real directional movement.

"What's different about the driver app architecture?"

The driver app needs ACCESS_BACKGROUND_LOCATION — because the driver might take a call while waiting for a rider. From Android 11, this permission requires a trip to device Settings. The driver app must guide the user through that path with clear UX, not just show a permission dialog. The driver app also needs actionable FCM notifications — tapping "Accept" directly from the notification, without opening the app. That's implemented with a PendingIntent on the notification action button, handled by a BroadcastReceiver that updates the TripState in Room and sends the acceptance to the server via WorkManager.

"How would you handle the driver going offline mid-trip — phone dies, app crashes?"

The server stops receiving location updates from the driver. After a configurable timeout (say, 30 seconds of silence), the server marks the driver as offline. The rider sees a "Tracking paused" state — the car marker freezes. If the driver reconnects, the stream resumes. If they don't reconnect within a longer window (say 2 minutes), the server triggers a trip reassignment flow and notifies the rider via FCM.

"How would you design the surge pricing display?"

Surge multipliers are computed server-side and change infrequently — every few minutes. The client doesn't need a real-time stream for this. On the home screen, the app makes a REST call to fetch the current surge zones (as LatLng polygons with associated multipliers). These are rendered as a heatmap overlay on the map. The client polls for updates every 30 seconds on the home screen, and stops polling once the rider enters a trip. This is a case where REST polling is more appropriate than a persistent stream — the data changes slowly and doesn't justify a permanent connection.


Quick Interview Checklist

  • ✅ Clarified scope — rider app, driver app, or both; core flows only
  • ✅ Clean Architecture + MVVM, Room as single source of truth for trip state
  • ✅ Named the three permission tiers — foreground, while-in-use, background
  • ✅ Called out Android 10, 11, and 14 restriction changes specifically
  • FusedLocationProviderClient over raw GPS — sensor fusion, adaptive power
  • PRIORITY_HIGH_ACCURACY during trip, BALANCED_POWER_ACCURACY pre-trip
  • smallestDisplacement filter to skip stationary updates
  • ✅ Foreground Service with foregroundServiceType="location" — required from API 29
  • stopWithTask="false" — survives swiping the app from recents
  • ✅ Service starts on DRIVER_ASSIGNED, stops on TRIP_COMPLETED
  • ✅ gRPC over WebSocket — Protobuf efficiency, HTTP/2 multiplexing, flow control
  • ✅ Kotlin Flow + Coroutines integration for gRPC streams
  • ✅ Marker smoothing — SphericalUtil.interpolate(), bearing animation, dead reckoning timeout
  • ✅ Batch marker updates within 500ms window for multi-driver map
  • ✅ Trip state machine as sealed class — one observable source drives both UI and service lifecycle
  • ✅ Process kill recovery — Room state + REST reconciliation on relaunch
  • ✅ Offline resilience — last Room state, NetworkCallback for reconnect
  • clientRequestId for idempotent ride requests on retry
  • ✅ FCM data messages — suppress if user is on tracking screen, update Room first
  • ✅ Adaptive update frequency by trip state and driver distance
  • ✅ No explicit WakeLock — Foreground Service handles process priority
  • ✅ Doze mode — Foreground Service is exempt, WorkManager is not
  • ✅ Map tile prefetching for the expected route on DRIVER_ASSIGNED
  • ✅ Kalman filter for GPS jitter at high zoom levels

Conclusion

Designing Uber's Android app tests whether you understand how real-time, location-aware features interact with the Android platform's constraints — not just whether you know the domain.

Knowing that PRIORITY_HIGH_ACCURACY should only be active during the trip, not on the home screen. Knowing the Foreground Service needs foregroundServiceType="location" or it silently stops receiving updates. Knowing that gRPC Protobuf is meaningfully more battery-efficient than JSON over WebSocket when multiplied across millions of updates. Knowing that GPS jitter at zoom level 18 needs a smoothing filter, not just interpolation. These are what senior Android engineers know — and what interviewers at Uber, DoorDash, and Google are listening for.

The design pillars:

  1. Room as the single source of truth — trip state survives process kills and network drops
  2. FusedLocationProviderClient with adaptive accuracy — GPS only when needed, network location when it isn't
  3. Foreground Service with correct manifest declaration — the only reliable way to track location through backgrounding
  4. gRPC bidirectional streaming — efficient, flow-controlled, multiplexed location delivery
  5. Trip state machine as a sealed class — drives both the UI and service lifecycle from one observable
  6. Marker smoothing with SphericalUtil.interpolate() — the difference between a car that glides and one that teleports
  7. Client idempotency keys — no duplicate ride requests on network retry


Frequently Asked Questions

What is FusedLocationProviderClient and why use it instead of LocationManager?

FusedLocationProviderClient is Google Play Services' intelligent location API that fuses GPS, Wi-Fi, cell towers, and device sensors to produce the most accurate location at the lowest possible battery cost. It is the officially recommended location API on Android.

Why not raw LocationManager:

  1. No sensor fusionLocationManager locks you into one source. No automatic fallback to network-based location when GPS is unavailable
  2. Always-on GPSLocationManager keeps the GPS chip fully active regardless of context. FusedLocationProviderClient reduces fix rate automatically when the device is stationary
  3. No deduplication — if another app already has a recent location fix, FusedLocationProviderClient reuses it rather than firing the GPS chip again
  4. No smallestDisplacement filterFusedLocationProviderClient's LocationRequest lets you skip updates when the device hasn't moved a configurable distance, eliminating idle battery drain

For Uber's use case, the LocationRequest configuration changes by trip state:

Trip statePriorityIntervalDisplacement filter
Home screenBALANCED_POWER_ACCURACY15s50m
Driver en routeHIGH_ACCURACY8s10m
Trip in progressHIGH_ACCURACY4s5m

Why does Uber use a Foreground Service for location tracking — and what happens without one?

A Foreground Service keeps the process alive and location accessible even when the user switches apps. Without it, Android kills background location access after a few minutes.

What happens without a Foreground Service:

  1. Rider opens Uber, trip starts
  2. Rider receives a phone call — switches to the Phone app
  3. Android 8+ background service restrictions kick in — Uber's background service is killed after ~1 minute
  4. Location tracking silently stops — the car marker freezes
  5. The rider has no idea where their driver is

Why a Foreground Service solves this:

  1. Higher OS process priority — the system will not kill it for memory reclamation under normal conditions
  2. Location access is maintained even with the app in the background
  3. The persistent "Trip in progress" notification is the required trade-off — and correct UX

Two manifest attributes that are easy to get wrong:

  1. android:foregroundServiceType="location" — required from Android 10 (API 29). Without it, the service does not receive location updates when targeting API 29+
  2. android:stopWithTask="false" — keeps the service running if the user swipes the app off the recents screen. Without it, an accidental swipe ends all tracking

The service starts on DRIVER_ASSIGNED and stops on TRIP_COMPLETED or TRIP_CANCELLED.


What is the difference between Android location permission tiers — and why does the driver app need more than the rider app?

Android has three distinct location permission tiers, each granting access in different app states. Getting this wrong on the driver app is a production failure.

Permission tierAccess whenUsed for
ACCESS_FINE_LOCATION (foreground)App is visible or Foreground Service runningRider app — watching the tracking screen
While-in-use (Android 10+ default)App is in the foreground onlyGeneral location features
ACCESS_BACKGROUND_LOCATIONAny time — including screen-offDriver app — location when driving with phone pocketed

Why the rider app only needs foreground location:

The rider watches the tracking screen during a trip. A Foreground Service with ACCESS_FINE_LOCATION keeps location access alive if they briefly switch apps. Background location is not required.

Why the driver app needs ACCESS_BACKGROUND_LOCATION:

A driver may be on the phone, checking messages, or have the screen off while waiting for a rider. Location must continue updating regardless. This requires explicit background location permission.

The Android version problem:

  1. Android 10 — you could request background location alongside the standard dialog
  2. Android 11 — the system dialog no longer offers "Allow all the time." Users must navigate to device Settings manually. The driver app must guide them there with clear UX
  3. Android 14 — accessing precise location from a background context without explicit ACCESS_BACKGROUND_LOCATION throws a SecurityException

Why use gRPC instead of WebSockets for location streaming on Android?

gRPC offers four concrete advantages over WebSockets for high-frequency location data: smaller payloads, HTTP/2 multiplexing, bidirectional streaming, and built-in flow control.

1. Serialisation efficiency:

A location update in Protobuf: ~15–20 bytes The same object in JSON over WebSocket: ~80–120 bytes

At 4-second update intervals across millions of active trips, smaller payloads mean the radio finishes faster and drops to a lower power state sooner — a real battery benefit on mobile.

2. HTTP/2 multiplexing:

Multiple gRPC streams share one TCP connection. The driver's outgoing location stream and incoming route updates coexist on one connection — no separate socket overhead for each.

3. Bidirectional streaming in one call:

A single DriverTracking gRPC call streams location up and receives server events (route changes, trip status) back down the same connection.

4. Built-in flow control:

HTTP/2 handles backpressure natively. If the server is slow, it signals the client to reduce send rate — no manual handling needed.

When to use WebSockets instead:

If the team wants to avoid the gRPC dependency overhead, or if existing infrastructure is WebSocket-based. In that case, WebSocket with binary MessagePack serialisation recovers most of the efficiency benefit.


How does map marker smoothing work in the Uber Android app?

Without smoothing, the car marker teleports between GPS positions every 3–5 seconds. With smoothing, a ValueAnimator interpolates the marker's position continuously between updates, producing the gliding car effect.

How to implement it:

  1. On each location update, record the current marker position as the animation start
  2. Start a ValueAnimator running for the expected update interval (e.g. 4 seconds)
  3. On each animation frame, call SphericalUtil.interpolate(startLatLng, endLatLng, fraction) from the Google Maps Android Utilities library
  4. Update the marker position to the interpolated coordinate
  5. Simultaneously interpolate the bearing — rotate the car icon to match the driver's heading

Why SphericalUtil.interpolate() instead of linear interpolation:

Linear interpolation on raw lat/lng values is geometrically incorrect for distances greater than a few metres — the Earth is not a flat plane. SphericalUtil interpolates along a great-circle arc, producing accurate geographic movement.

Dead reckoning when updates stop:

If a location update is overdue (the driver went through a tunnel), continue the ValueAnimator past the target position using the last known speed and heading. The car keeps moving in the expected direction rather than freezing. Cancel the dead-reckoning animator when the next real update arrives.

At high zoom levels (GPS jitter):

GPS accuracy is typically 3–5 metres. At zoom level 18–19, that noise becomes visible as jitter. Apply an exponential moving average or Kalman filter to incoming positions before feeding them to the animator. This smooths noise without delaying real directional movement.


How does the trip state machine work and why model it explicitly?

An explicit trip state machine — implemented as a Kotlin sealed class — prevents the scattered if-else chains that make lifecycle code fragile. Every valid trip state and its transitions are declared in one place.

kotlin
sealed class TripState {
    object Idle : TripState()
    object Requesting : TripState()
    data class DriverAssigned(val driver: DriverInfo, val eta: Int) : TripState()
    data class DriverEnRoute(val driver: DriverInfo, val location: LatLng) : TripState()
    data class DriverArrived(val driver: DriverInfo) : TripState()
    data class TripInProgress(val driver: DriverInfo, val route: List<LatLng>) : TripState()
    data class TripCompleted(val receipt: Receipt) : TripState()
    data class Error(val reason: String) : TripState()
}

Why a sealed class is the right choice:

  1. Exhaustive when expressions — the compiler forces handling of every state. No silent unhandled case
  2. Typed state dataDriverAssigned carries DriverInfo and eta. TripInProgress carries the active route. No stripping state data out of shared nullable fields
  3. Single observable source — the ViewModel exposes StateFlow<TripState>. The UI, Foreground Service lifecycle, and notification logic all react to the same stream

How the state machine drives everything:

  1. UI renders the correct screen for each state — map, booking, tracking, receipt
  2. Foreground Service starts on DriverAssigned, stops on TripCompleted
  3. FCM notifications are suppressed if the state is already TripInProgress and the user is watching the screen

What happens if the Android app is killed mid-trip?

Room is the single source of truth — every state transition is persisted before acknowledgement. On relaunch, the app reads Room to restore the last known state, then makes a REST call to reconcile with the server's current authoritative state.

The full recovery flow:

  1. App is killed mid-trip (low memory, user force-stops, OS reclamation)
  2. User relaunches the app
  3. ViewModel reads TripState from Room — restores to e.g. TripInProgress with the last known driver location
  4. App displays the tracking screen immediately — no blank screen, no navigating to home
  5. App makes GET /trips/current to the server — reconciles the actual current state (the trip may have progressed, the driver may have arrived)
  6. The gRPC stream reconnects and real-time updates resume from the current position

Why both Room and REST are needed:

Room alone holds only the state at the moment of the kill. The trip may have progressed during the outage. The REST reconciliation syncs the gap. The gRPC stream then takes over for ongoing updates.


How does battery optimisation work for continuous GPS and network use?

Battery drain from a ride-hailing app comes from three sources: GPS chip, cellular radio, and CPU. Each can be meaningfully reduced without sacrificing UX.

Adaptive GPS accuracy by state:

  1. Home screen → PRIORITY_BALANCED_POWER_ACCURACY — uses network location, no GPS chip
  2. Driver en route (far away) → PRIORITY_HIGH_ACCURACY at 8-second intervals — GPS on, but less frequent
  3. Trip in progress → PRIORITY_HIGH_ACCURACY at 4-second intervals — maximum precision needed

smallestDisplacement filter:

Set to 5 metres during a trip. If the driver is stopped at a red light, GPS updates are skipped entirely — the position hasn't changed enough to matter. This eliminates a large fraction of idle updates with zero UX cost.

gRPC's persistent HTTP/2 connection:

Every avoided connection setup (TCP handshake + TLS) is a battery saving. Protobuf's 15–20 byte payload vs JSON's 80–120 bytes means the radio finishes each transmission faster and drops to low-power state sooner.

No explicit WakeLock:

The Foreground Service maintains the required process priority. Explicit WakeLocks are a battery antipattern — they are frequently held longer than intended and prevent the CPU from entering deep sleep states between GPS updates.

Doze mode:

Foreground Services are explicitly exempt from Doze restrictions — they keep running and receiving location updates even with the screen off. WorkManager and AlarmManager are not exempt, which is why a Foreground Service is required for active trip tracking.


Which companies ask the Uber Android app system design question?

Uber, Lyft, DoorDash, Google, Snap, and Meta ask variants of this question for senior Android engineer roles.

Why it is one of the most revealing Android system design questions:

  1. Requires genuine platform depth — permission tiers across Android versions, Foreground Service manifest attributes, FusedLocationProviderClient configuration — these are not guessable
  2. Tests real-time architecture reasoning — gRPC vs WebSocket requires first-principles reasoning about serialisation efficiency, multiplexing, and flow control; not library name-dropping
  3. Combines client and distributed systems — the location pipeline spans the device, a persistent connection, server-side fan-out, and another device's map — a complete distributed system hiding inside a mobile app

What interviewers specifically listen for:

  1. Three permission tiers + Android 10/11/14 changes — not just "we need location permission"
  2. foregroundServiceType="location" and stopWithTask="false" — the manifest attributes that are easy to get wrong
  3. gRPC reasons beyond "it's better" — Protobuf size, HTTP/2 multiplexing, built-in flow control
  4. SphericalUtil.interpolate() not linear interpolation — and the dead-reckoning timeout
  5. Room + REST reconciliation on process kill — the two-step recovery, not just "Room has the state"

If any of those five feel uncertain when explaining them live, Mockingly.ai runs Android system design simulations for engineers preparing for senior roles at Uber, Google, DoorDash, and Lyft — with follow-up questions on exactly these points.


Knowing these concepts is one thing. Explaining them clearly under interview pressure — while drawing diagrams and fielding follow-ups in real time — is a different skill entirely. Mockingly.ai has Android-focused system design simulations for senior engineers preparing for interviews at Uber, Google, DoorDash, Lyft, and beyond.

Prepare for these companies

This topic is commonly asked at these companies. Explore their full question lists:

Interview Guide for This Topic

See how top companies test this question in their system design interviews:

Practice "Design Uber's Mobile App (Android)" with AI

Reading is great, but practicing is how you actually get the offer. Get real-time AI feedback on your system design answers.

Related Posts

Turn This Knowledge Into Interview Confidence

Reading guides builds understanding. Practicing under interview conditions builds the muscle memory you need to deliver confidently when it counts.

Free tier available • AI-powered feedback • No credit card required

Practice system design with AI