Skip to content
All essays

Mobile architecture

Design Uber on iOS: a system design interview answer from the hiring side

"Design Uber" is not scored on the map. It is scored on one question: what does the driver's pin mean when it is 21 seconds old? A worked iOS answer, minute by minute, with the follow-ups interviewers use.

10 min read

Most candidates draw the same picture for "design Uber": a map view, a view model, a location manager, a WebSocket, a backend box with dispatch inside it. It looks complete. Then I ask: the rider is watching the little car, and the last position you received is 21 seconds old. What does the screen say?

I have run 500+ technical interviews from the hiring side over 15 years in mobile. In this round, that question separates levels faster than anything on the diagram. The pin on the map is a claim. Its age decides what it is allowed to claim.

This is a worked answer: one prompt, walked the way I would want a senior candidate to walk it in 45 minutes. For an offline-first prompt on iOS, see the messaging app worked answer. The Android round, with an offline field app, is in the Android worked answer.

  • The prompt: design the iOS rider and driver apps for a ride-hailing service.
  • The shape: requirements, trip state authority, a freshness budget, streaming with resync, location in the background, battery and heat, the map SDK, observability, rollout.
  • The test underneath: can you say what the user is told when the data is old, and who decides.

The 45-minute round, minute by minute

The trap in this prompt is the backend. Matching drivers to riders is a famous problem, and candidates spend twenty minutes on it. Nobody asked. The client has more than enough decisions for 45 minutes.

MinutesWhat you doWhat the interviewer is checking
0 to 5Clarify: rider app, driver app or both; cities; devicesDo you ask before you build?
5 to 10Requirements, and what you will not designCan you cut?
10 to 18Trip state: what the device may show, what the server confirmsWho owns the truth?
18 to 26The freshness budget and the realtime channelWhat does the UI claim when data is late?
26 to 34Location in the background, battery and heatDo you know iOS's limits?
34 to 40The map SDK boundary, observabilityHave you run this in production?
40 to 45Rollout and the trade-offs you acceptedCan you summarise a decision?

Say the plan in the first minute. The interviewer will pull you off it. The plan is how you get back.

Requirements: say what you will not build

Questions first. Are we designing the rider app, the driver app, or both? How many cities, and do drivers use older phones? What must keep working in a tunnel or a parking garage? Does the rider need to see the trip from the lock screen?

  • Functional, rider: request a ride, see the assigned driver approach, follow the trip, see the fare.
  • Functional, driver: go online, accept a request, navigate, mark arrived, start and end the trip.
  • Non-functional: the rider never sees a confident ETA built on stale data; the driver's phone lasts a shift in a windshield mount; a lost connection never loses a trip.
  • Out of scope: matching and pricing on the server, payments processing, the map renderer itself. I will state the API contract I need.

Saying "matching is out of scope" out loud is a senior move. It protects the 40 minutes that are actually yours.

Trip state: what the device may show, what the server confirms

Start with the trip as a state machine: requested, accepted, arriving, arrived, in progress, completed, cancelled. The decision interviewers listen for is not the list. It is who may move it.

TransitionWho proposesWho confirmsMay the screen show it early?
Driver arrivedDriver taps, or a geofenceServerYes, on the driver's screen at once; the rider sees it once confirmed
Trip startedDriverServerYes on the driver's screen, marked as pending
Trip completedDriverServer onlyNo. It moves money
CancelledRider or driverServerShown as "cancelling" until confirmed

The rule: the device proposes, the server ratifies, and money-moving transitions are never client-authoritative. The driver's own screen honours its tap immediately, so the app feels fast, and the rider sees what the server has confirmed. A GPS fix is evidence the server can weigh, never a confirmation on its own, because a fix can be spoofed.

The freshness budget: what the pin means when it is old

This is where the round is decided. A position is not true or false. It is old or new, and the screen has to change what it says as it ages. Give it numbers:

Position ageWhat the rider sees
Under 6 secondsLive pin, moving smoothly, with the ETA
6 to 20 secondsThe pin stays where it was, stops animating, and the screen says "Updating"
Over 20 secondsNo minute count. The screen says "Locating driver"

The exact numbers are a product decision. Having numbers is the engineering one. Measure the age against the server's clock, from when it received the fix, not against the device clock.

Swift
struct DriverFix {
    let coordinate: CLLocationCoordinate2D
    let receivedAt: Date   // server time when the fix arrived
}

enum PinState {
    case live(CLLocationCoordinate2D)       // under 6 s: animate, show the ETA
    case updating(CLLocationCoordinate2D)   // 6 to 20 s: keep the pin, stop animating
    case locating                           // over 20 s: no minute count
}

func pinState(for fix: DriverFix?, serverNow: Date) -> PinState {
    guard let fix else { return .locating }
    let age = serverNow.timeIntervalSince(fix.receivedAt)
    switch age {
    case ..<6: return .live(fix.coordinate)
    case ..<20: return .updating(fix.coordinate)
    default: return .locating
    }
}

serverNow is the device clock corrected by the offset measured when the connection opened. Say that sentence, because the follow-up "what if the phone's clock is wrong?" is coming.

Realtime: a stream, plus a way to catch up

Polling every few seconds works in a demo and costs battery and server capacity at rush hour. The answer is a WebSocket, URLSessionWebSocketTask on iOS, carrying position updates whose rate adapts: fast near pickup and during the trip, slow while the rider is waiting for a match, slower still when the network is congested.

The stream is not enough on its own. Tunnels, parking garages and a suspended app all drop it, and an old app version may not speak it at all. So every frame carries a sequence number, and a reconnect asks for everything after the last one the app has applied. The detail that separates levels is when the app records that number:

Swift
actor TripStream {
    private let store: PositionStore
    private(set) var cursor = 0   // last sequence the app has stored

    init(store: PositionStore) { self.store = store }

    func consume(_ frames: AsyncThrowingStream<PositionFrame, Error>) async throws {
        for try await frame in frames where frame.sequence > cursor {
            try await store.save(frame)   // durable first
            cursor = frame.sequence       // then the cursor moves
        }
    }
}

Move the cursor when a frame arrives instead of after it is stored, and an app killed between the two believes it is current while it missed thirty seconds of movement. That is exactly how a pin freezes while the car keeps driving.

Location in the background, battery and heat

Here candidates describe iOS as if it were a server. The accurate version, for the two apps:

  • Driver app. It needs continuous location during a shift. That means the location background mode, allowsBackgroundLocationUpdates, the system's blue location indicator, and an honest permission request. On iOS 17 and later, CLLocationUpdate.liveUpdates() with a CLBackgroundActivitySession is the modern way to keep updates flowing.
  • Rider app. It does not need the rider's location in the background at all. Once the rider leaves the app, it is suspended and its socket closes. The trip stays visible on the lock screen through a Live Activity, updated by push from the server, and the app resyncs from its cursor when it returns.
  • Significant-change monitoring is the low-power fallback: coarse, but it can relaunch an app the system terminated.

Then the budget. Continuous high-accuracy GPS, a bright screen and a hot windshield mount are how a phone starts throttling and stops reporting. A senior answer makes accuracy a dial with a rule:

Swift
struct LocationPolicy {
    let accuracy: CLLocationAccuracy
    let distanceFilter: CLLocationDistance   // metres between updates
    let degraded: Bool                       // reported to the server
}

func locationPolicy(thermal: ProcessInfo.ThermalState, lowPower: Bool) -> LocationPolicy {
    switch thermal {
    case .nominal where !lowPower:
        return LocationPolicy(accuracy: kCLLocationAccuracyBest, distanceFilter: 5, degraded: false)
    case .nominal, .fair:
        return LocationPolicy(accuracy: kCLLocationAccuracyNearestTenMeters, distanceFilter: 20, degraded: true)
    default:
        return LocationPolicy(accuracy: kCLLocationAccuracyHundredMeters, distanceFilter: 50, degraded: true)
    }
}

The app watches ProcessInfo.thermalStateDidChangeNotification and Low Power Mode, applies the policy, and tells the server it has degraded. Degrading is a state dispatch can see, not a silent decision that leaves it guessing.

The map SDK: useful, and never in charge

A third-party map and routing SDK draws the map, computes routes and estimates arrival times. It is also the largest dependency in the app, updated on the vendor's schedule. The design question is the boundary.

Put it behind your own RouteProvider interface, so no screen and no trip logic imports the vendor's types. Its outputs, a route and an ETA, are advice the trip logic may accept or reject. It can never write trip state. Then give it an exit: a remote flag that switches the provider per cohort, to the last good version or to a coarse ETA computed on the server, with no app release.

Why this earns points: when a vendor release starts burning CPU on one device family, the phones heat up, positions arrive late and pins freeze. With the boundary and the flag, the fix is a configuration change for the affected cohort in minutes. Without them, it is an App Store release and a message asking drivers to update, which helps neither drivers who are offline nor those who will not update today.

Observability and rollout

Name the numbers. Position age at p99, which is the freshness budget as a metric. Time in degraded mode per trip. Battery drain per active hour, by device model. Reconnects and resync size. Disagreements between what a device proposed and what the server confirmed. Crash-free sessions. All of them segmented by region, device model and app version, because a failure that disappears in the global average is often obvious in one city on one phone.

Rollout: the streaming client ships behind a remote flag, off by default, with polling kept as the fallback. Internal drivers first, then a small share of one city, watching position age and battery. App Store phased release controls how fast the binary spreads; the flag controls how fast the behaviour does and can be turned off without a release. Expect several app versions in the field at once, and write down when the old polling path will be removed.

How the Android answer differs

The trip model, the freshness budget, server authority and the SDK boundary carry over unchanged. Two platform answers change. The driver app keeps location flowing with a foreground service and its persistent notification, which is the Android contract for work the user should know about. And the process can be killed mid-trip and restored, so the trip screen rebuilds from the stored trip and cursor, never from a view model's memory. The Android worked answer walks that platform in depth with a different prompt.

Mid, senior and staff: what the answers sound like

TopicMid-levelSeniorStaff
Trip state"The app updates the status."Device proposes, server ratifies; completion never on the client.Names who resolves disagreements and the metric that shows them.
Freshness"Use a WebSocket."Stream plus resync; cursor moves after the frame is stored.A position-age budget with UI states, alarmed per cohort.
Battery"Lower the frequency in the background."Accuracy policy by thermal state and Low Power Mode.Per device tier battery budget with an owner and a reported degrade state.
Map SDKCalls the SDK from the screens.Wrapped behind an interface.Switchable per cohort with a permanent fallback; no release needed to reverse.

The staff column is not more components. It is the same design with owners, budgets and a way to know when it is wrong.

The follow-ups interviewers use to push

  • "The last position is 21 seconds old. What does the rider see?" Tests the freshness budget.
  • "The phone's clock is two minutes off." Tests whether age is measured against server time.
  • "The driver drives through a tunnel and the app is killed." Tests the cursor and the resync.
  • "The driver taps completed with no signal." Tests what may be shown early and what may not.
  • "The phone is overheating in the windshield mount." Tests the accuracy policy and the reported degrade.
  • "The map vendor's new release drains batteries on one phone model." Tests the SDK boundary and the per-cohort switch.

Prepare one sentence for each. If a follow-up makes you draw a new box, your design was missing a decision. If it makes you point at a box that already handles it, you are having the conversation the round is for.

Questions engineers ask about the "design Uber" interview

How do you design Uber in a mobile system design interview?

Design the client, not the dispatch backend. Name who owns trip state, set a freshness budget for the driver's position and say what the screen shows when it is exceeded, stream positions with a resync path, keep location flowing in the background without draining the phone, and isolate the map SDK so a vendor bug cannot break a trip.

What do interviewers look for in a ride-hailing design?

Three things most candidates skip: a number for how stale a position may be before the UI changes what it claims, a clear line between transitions the device may show early and the ones only the server may confirm, and a battery budget with a rule for degrading accuracy.

How is the Android answer different?

The trip model, the freshness budget and the server authority stay the same. On Android, the driver app keeps location flowing with a foreground service and a visible notification, and process death mid-trip is a normal event. The offline field app version of the Android round is worked through separately.

How should I practise this round?

Answer it out loud against a 45-minute timer, then change one fact: the phone is overheating, the driver loses signal in a tunnel, or the map vendor ships a bad release. Answer again. The second pass shows which parts of your design were decisions and which were boxes.

Share this essay