Uber Interview Questions · 2026

Uber Interview Questions (2026): What They Actually Ask

A backend engineer who went through Uber's loop in late 2024 described the experience this way: "Every problem had a hidden layer. They'd give you a standard graph traversal problem but frame it as 'there are a million drivers and you need to find the 10 nearest ones in under 50 milliseconds.' You couldn't just think in the abstract." That framing tells you most of what you need to know about Uber interviews.

Uber's process is one of the more product-grounded loops in big tech. The coding problems skew toward real-world scenarios rather than pure puzzles, the system design questions pull directly from Uber's infrastructure (ride-matching, geospatial indexing, surge pricing, real-time dispatch), and the behavioral round is calibrated against a specific set of cultural values the company refreshed under Dara Khosrowshahi. This page covers the 2026 loop based on verified candidate reports from Glassdoor, interviewing.io, Blind, LeetCode Discuss, and several first-person Medium accounts from 2024 to early 2026.

4-6 weeksProcess
4-6Rounds
LC Medium-HardCoding
Virtual onsiteFormat

Easy questions

14

Sort by start time. Iterate through: if the current interval starts at or before the end of the last merged interval, extend the end to the max of both ends. Otherwise, start a new merged interval. Edge cases to discuss: single interval, totally contained intervals, and intervals that share exactly one endpoint (most interviewers at Uber want [1,3] and [3,5] merged to [1,5]).

Shows up in coding screens and OAs. Uber sometimes frames it as "merge overlapping surge zones in a city map" which is the same problem with geographic labels on the intervals.

The "trip obsessed" value. The customer doesn't have to be an Uber rider; it can be an internal customer, a downstream team, or an external API consumer. What matters is that you can tie the technical work to a measurable improvement in someone's experience.

Weak answers describe infrastructure improvements without connecting them to user impact. Strong answers name the specific failure mode users experienced ("drivers were seeing stale pickup ETAs because the cache TTL was too long, causing 15% of pickups to be 3+ minutes late"), describe the fix, and quantify the improvement. A 15% reduction in late pickups is a story. "I improved caching performance" is not.

The interviewer is checking for defensiveness. A story where you immediately agreed the feedback was correct and changed everything about how you work reads as either fabricated or as someone with no independent judgment. A story where you understood the feedback, asked a clarifying question or two, and made a specific change, even a partial one, reads as credible.

Performance review feedback works here, but a sharper answer often comes from feedback given informally, in a code review, in a retro, in a one-on-one, since it shows the feedback loop you operate in day to day rather than a formal process you only encounter once a year.

The "bold bets" value. Uber historically rewards engineers who stretch beyond their defined role, particularly those who identify a gap in the system that no one owns and just fix it without waiting for permission. A story about picking up an unfamiliar codebase, shipping a fix under time pressure, and landing it cleanly works well here.

The follow-up often goes toward: "What did you learn that you didn't expect to?" Have a real technical answer. Learning that you should always read the runbook before making changes to a production config is more credible than "I learned the value of teamwork."

Uber is in a genuinely interesting technical moment: autonomous vehicle integration (Uber Advanced Technologies Group spun out but Uber still has AV partnerships), the expansion of Uber Eats into non-food verticals, the push into pre-scheduled rides and corporate travel, and the ongoing engineering work on their global dispatch and payment infrastructure. Any of these are valid hooks.

What doesn't work: generic "I love transportation technology" answers, anything that confuses Uber's current technical direction with Waymo or Lyft, or answers that name products Uber has deprecated. Research the team you're interviewing with specifically. If you're interviewing for the marketplace engineering team, know what their tech stack looks like and what problems they've published about on Uber's engineering blog.

This one doubles as a soft technical round. Interviewers use your answer to probe depth: say "I built a distributed caching layer" and expect direct follow-up questions about cache invalidation, consistency, and failure handling, not just narrative details about the project timeline.

Pick the project where you can go three levels deep on any part of it if asked, not the project with the most impressive-sounding title.

Most candidates complete the process in four to six weeks from application to verbal offer. The online assessment is usually sent within one to two weeks of the recruiter screen. After the virtual onsite, the hiring committee review takes three to five business days. If you have a referral, the OA link sometimes comes faster and the debrief may move quicker. The longest waits tend to happen between the onsite and the decision call when the committee needs additional discussion.

Yes, as of 2024 to 2026, Uber uses CodeSignal for both the online assessment and the live technical phone screen. Some teams or regions have used HackerRank for the OA in the past, but CodeSignal is the primary platform. During the live screen, you code in CodeSignal while on a Zoom call with the interviewer. The environment supports most common languages including Python, Java, Go, and C++.

System design is a full dedicated round for L4 and above. For L3 new grads, it's either absent or appears as a lighter architecture discussion appended to a coding round. At L5 and above, expect two system design-flavored rounds: one high-level distributed systems design and one low-level design or machine coding exercise where you implement a service or module in working code. Senior and staff candidates sometimes get an additional domain-specific round focused on Uber's marketplace or geospatial infrastructure.

Python, Java, and Go are the most commonly used by candidates and are well-supported by Uber interviewers. C++ is accepted but less common outside systems-adjacent roles. JavaScript is generally accepted for the OA but some interviewers prefer you use a backend language for the live screen. If you're interviewing for a role where the team uses Go heavily (which is true for a lot of Uber's backend services), using Go in your live screen signals genuine familiarity with the team's actual stack.

Amazon's leadership principles round is systematic: interviewers go through a defined rubric and expect STAR format answers mapped to named principles like "Customer Obsession" or "Invent and Simplify." Uber's behavioral round is less formulaic in structure but just as rigorous in substance. Uber interviewers push harder on specific data and numbers, expect you to describe real trade-offs rather than just positive outcomes, and will often turn a behavioral answer into a 15-minute technical discussion if your story has technical depth. The STAR format is still useful at Uber, but the result component needs real quantitative data, not a vague qualitative improvement.

The OA sits at medium to medium-hard. The live coding rounds also land mostly in medium territory, with occasional hard-difficulty problems at senior levels. What makes Uber's coding rounds feel harder than the difficulty label is the real-world framing (you're solving a driver-routing problem, not "find the shortest path in a graph") and the optimization pressure in follow-ups. A clean medium solution that you can defend and optimize on demand is more valuable here than a memorized hard solution you can't adapt.

A stack is last-in-first-out. You push and pop from the same end, so the most recently added item is the first one out. A queue is first-in-first-out. You add at the back and remove from the front, so items come out in the order they arrived. Both give O(1) insert and O(1) remove when implemented with an array-backed ring buffer or a doubly linked list, the difference is purely which end you're allowed to touch.

In a system like Uber's dispatch pipeline, queues show up constantly. When a rider requests a trip, that request typically lands on a message queue (something like Kafka or SQS) before a matching service picks it up, which preserves ordering and lets you buffer bursts of demand without dropping requests. A stack is less common in the request path but shows up in things like undo history for a support agent tool, or in the call stack itself when a service does recursive route computation.

The gotcha interviewers look for is whether you understand that "queue" in casual conversation usually means a message queue with at-least-once delivery and possible reordering across partitions, which is a very different animal from the strict FIFO queue data structure you'd implement in an algorithms round. Being able to draw that line shows you can move between the CS-fundamentals framing and the distributed-systems framing without conflating them.

A hash map runs each key through a hash function that produces an integer, then uses that integer modulo the table size to pick a bucket. If the hash function spreads keys uniformly and the table isn't too full, most buckets hold zero or one entry, so a lookup is just "hash the key, jump to the bucket, compare." That's where the average O(1) comes from. It's an average, not a guarantee, because a badly distributed hash or a small table can degrade to O(n) if every key lands in the same bucket.

Collisions are handled one of two main ways. Separate chaining keeps a small linked list (or a tree, as Java's HashMap does once a bucket gets past eight entries) at each bucket, so colliding keys just get appended and a lookup walks the short chain. Open addressing instead finds another slot in the same array, using linear probing, quadratic probing, or double hashing, which keeps everything in one contiguous block of memory and tends to be more cache-friendly but degrades faster as the table fills up.

Worth mentioning if it comes up in a design context: real hash tables resize and rehash once the load factor crosses a threshold, usually around 0.7, which is an amortized O(n) operation that happens rarely enough not to affect the average-case analysis. For something like a geospatial lookup of nearby drivers, you'd typically layer a spatial structure like a geohash or H3 index on top of hashing rather than relying on raw key equality, since you need range queries, not just exact-match lookups.

Medium questions

21

Maintain two deques tracking the current window's maximum and minimum values. The window is valid as long as max minus min is at most K. When the window becomes invalid, shrink from the left until it's valid again. Each element enters and exits each deque once, giving O(n) overall.

This specific problem was reported in a December 2024 L4 backend onsite at Uber. The initial brute-force attempt ran O(n^4). The interviewer prompted optimization in two stages, first to O(n^2), then to the deque-based O(n) approach. The interviewer specifically asked: "What's the invariant you're maintaining in your deque?"

python
from collections import deque

def longest_subarray(nums, k):
    max_dq, min_dq = deque(), deque()
    left = 0
    result = 0

    for right in range(len(nums)):
        # maintain decreasing deque for max
        while max_dq and nums[max_dq[-1]] <= nums[right]:
            max_dq.pop()
        max_dq.append(right)

        # maintain increasing deque for min
        while min_dq and nums[min_dq[-1]] >= nums[right]:
            min_dq.pop()
        min_dq.append(right)

        # shrink window if invalid
        while nums[max_dq[0]] - nums[min_dq[0]] > k:
            left += 1
            if max_dq[0] < left:
                max_dq.popleft()
            if min_dq[0] < left:
                min_dq.popleft()

        result = max(result, right - left + 1)

    return result

Compute Euclidean distances from each driver to the rider and maintain a max-heap of size K. For each driver, if the heap has fewer than K entries, push it. If the current distance is less than the max in the heap, pop the max and push the current driver. Return the heap contents when done.

Reported from multiple phone screens and onsite coding rounds. The real test comes in the follow-up: "How would you handle 500,000 active drivers?" The expected answer introduces geohashing to pre-partition drivers into geographic buckets, so you're only running the heap comparison within the relevant bucket and its immediate neighbors, not against the full driver set. Candidates who go straight to heap without addressing scale get pushed on it.

python
import heapq
import math

def k_nearest_drivers(drivers, rider, k):
    # drivers: list of (id, x, y), rider: (x, y)
    max_heap = []  # (-distance, id)

    for driver_id, dx, dy in drivers:
        dist = math.sqrt((dx - rider[0])**2 + (dy - rider[1])**2)
        heapq.heappush(max_heap, (-dist, driver_id))
        if len(max_heap) > k:
            heapq.heappop(max_heap)

    return [driver_id for _, driver_id in max_heap]

HashMap for O(1) key lookups combined with a doubly linked list to track recency. The head holds the most recently used entry; the tail holds the least recently used. On a get, move the accessed node to the head. On a put that exceeds capacity, evict the tail node and delete its key from the map.

A classic that shows up in Uber phone screens and OAs. Uber's follow-up here is usually: "How would you scale this to a distributed cache shared across multiple ride-matching servers?" That leads into a discussion of consistent hashing, cache invalidation on price updates, and whether you can tolerate stale driver availability data for a few hundred milliseconds.

java
import java.util.LinkedHashMap;
import java.util.Map;

class LRUCache extends LinkedHashMap<Integer, Integer> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
}

Use a sorted multiset (or a balanced BST equivalent) to maintain the current window of K elements. For each new element added and each element removed as the window slides, update the sorted structure in O(log K) time. The minimum absolute difference in the current window is found by checking adjacent elements in the sorted order. Overall time complexity is O(n log K).

Reported in an Uber phone screen. The brute force is O(n * K log K), which is too slow for large inputs. The key insight interviewers want to see is that you need a data structure that supports ordered insertion and deletion, not just a simple min or max query.

Build a directed graph where an edge from A to B means A appears before B in at least one of the given subsequences. Run topological sort (Kahn's algorithm using BFS). If the sorted output matches the original array and the topological order is unique (exactly one node with in-degree zero at each step), return true.

Reported from a 2024 Uber onsite. The uniqueness check is the gotcha: standard topological sort implementations don't verify uniqueness, you have to check at each BFS step that exactly one node has zero in-degree to confirm the sequence is determined.

python
from collections import defaultdict, deque

def can_reconstruct(original, seqs):
    graph = defaultdict(set)
    in_degree = {x: 0 for x in original}

    for seq in seqs:
        for i in range(len(seq) - 1):
            u, v = seq[i], seq[i+1]
            if v not in graph[u]:
                graph[u].add(v)
                in_degree[v] = in_degree.get(v, 0) + 1

    queue = deque([n for n in in_degree if in_degree[n] == 0])
    idx = 0

    while queue:
        if len(queue) > 1:
            return False  # not unique
        node = queue.popleft()
        if idx >= len(original) or node != original[idx]:
            return False
        idx += 1
        for neighbor in graph[node]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    return idx == len(original)

Precompute prefix sums for horizontal and vertical runs of consecutive 1s. For each cell (i, j), you know the maximum rightward and downward streak of 1s starting from that cell. Then iterate over all possible square sizes k from large to small: check if the top row, bottom row, left column, and right column all have at least k consecutive 1s using the precomputed streaks. First match is the answer.

Reported in the initial screening round for an L4 backend role in December 2024. The interviewer accepted an O(n^3) solution but asked for the optimization. The key observation is that you don't need to check all four sides independently once you have the streak arrays; the four corner-based lookups determine validity in O(1) per cell per size.

DFS or BFS from every unvisited land cell, marking every connected land cell you reach as visited so it doesn't get counted again. Each DFS or BFS call accounts for exactly one island. Time complexity is O(rows x cols) since every cell is visited once.

A warmup-tier problem at most companies, but Uber's version usually adds a twist: "now do it for a grid that updates in real time as new zones come online." That's Number of Islands II, which needs union-find instead of a fresh DFS pass every time land is added. The union-find follow-up is the actual bar here; the base DFS version is just the entry point.

BFS where each node is a word and edges connect words that differ by exactly one character. Build adjacency as you go: for each word in the queue, generate all one-character mutations and check if they exist in the dictionary. Track visited words to avoid cycles. The BFS level count when you reach the target is the answer.

Reported from Uber phone screens multiple times across 2024 to 2025 community threads. The optimization that comes up in follow-ups is bidirectional BFS, starting from both the source and target simultaneously and meeting in the middle, which can dramatically reduce the branching factor in large word dictionaries.

DFS with memoization. From each cell, try moving in all four directions to a cell with a strictly greater value. Cache the result for each cell so you don't recompute. The answer is the maximum cached value across all starting cells.

LeetCode 329. Reported from an Uber OA and confirmed in an interviewing.io session for a senior role. The trickier follow-up: how does this change if you allow diagonal moves? The algorithm structure is the same; you add four more direction pairs.

Kruskal's algorithm: sort all edges by weight, then use a union-find structure to greedily add edges that connect two different components. Stop when you have N-1 edges for N cities. Prim's algorithm is an alternative starting from a single node and repeatedly adding the cheapest edge that extends the current tree.

Reported from an Uber phone screen framed as "connecting delivery zones." The follow-up asked which algorithm you'd prefer when edges are already given as a sparse list (Kruskal) versus when you have a dense adjacency matrix (Prim). The difference in time complexity matters: Kruskal is O(E log E), Prim with a binary heap is O((V + E) log V).

Classic single-source shortest path. Run Dijkstra's algorithm from the source node using a min-heap keyed by distance. Pop the closest unvisited node, relax its outgoing edges, and repeat. Once the heap is empty, the answer is the maximum distance across all reachable nodes, or -1 if any node was never reached.

Uber frames this as computing how long it takes an ETA update or a fare change to propagate through a mesh of regional pricing services, same weighted graph, same Dijkstra. The follow-up worth practicing: what if edge weights can go negative (network latency can't, but pricing adjustments sometimes do)? That rules out Dijkstra and pushes the conversation toward Bellman-Ford, which is exactly the transition into the next question.

python
import heapq
from collections import defaultdict

def network_delay_time(times, n, k):
    graph = defaultdict(list)
    for u, v, w in times:
        graph[u].append((v, w))

    dist = {}
    heap = [(0, k)]

    while heap:
        d, node = heapq.heappop(heap)
        if node in dist:
            continue
        dist[node] = d
        for neighbor, weight in graph[node]:
            if neighbor not in dist:
                heapq.heappush(heap, (d + weight, neighbor))

    return max(dist.values()) if len(dist) == n else -1

Standard Dijkstra doesn't respect a hard cap on the number of edges used, so the accepted approach is a bounded Bellman-Ford: relax all edges K+1 times, tracking the best cost to reach each node within that many hops. Using a copy of the previous iteration's distance array when relaxing, rather than updating in place, is what stops a single iteration from chaining together more hops than allowed.

Uber's framing is usually a route with a fare cap tied to a maximum number of toll segments, or a maximum number of driver handoffs on a multi-leg delivery. (Side note: candidates who reach for a BFS-with-priority-queue hybrid instead of bounded Bellman-Ford almost always get the stop-count constraint wrong on the first pass, so budget extra time to trace through an example with K=1.)

python
def find_cheapest_price(n, flights, src, dst, k):
    prices = [float('inf')] * n
    prices[src] = 0

    for _ in range(k + 1):
        temp = prices[:]
        for u, v, cost in flights:
            if prices[u] != float('inf') and prices[u] + cost < temp[v]:
                temp[v] = prices[u] + cost
        prices = temp

    return prices[dst] if prices[dst] != float('inf') else -1

DFS with a recursion stack (a separate set of nodes on the current active call path) alongside a visited set. If DFS reaches a node already in the recursion stack, a cycle exists. After fully exploring from a node, remove it from the recursion stack. An undirected version only needs a visited set and parent tracking, not the separate stack.

Appears in Uber OAs and is often extended to "given a course schedule with prerequisites, can all courses be completed?" That's LeetCode 207, same algorithm with different framing.

This is the "owner mentality" question in its most direct form. Interviewers want to see that you noticed the problem (not that someone handed it to you), that you drove the investigation, that you made the fix or the architectural decision, and that you saw it through to verification in production. A story that ends at "I submitted the PR" doesn't finish the loop.

Strong answers include what monitoring or alerting surface you used to detect the problem, what your investigation process was, and what you put in place to prevent recurrence. An on-call story with concrete incident data works well here. Vague stories about "improving the deployment pipeline" without specific before/after data don't land at Uber.

Uber interviewers are explicitly checking for two things here: whether you can name a real failure without deflecting blame, and whether the "afterward" part shows a concrete behavior change, not just a lesson learned in the abstract. "I take more time to test now" is weaker than describing the specific new step you added to your own process, a checklist item, a new test, a review habit, and evidence you've actually used it since.

Pick a failure with real consequences, a missed deadline that cost a launch window, a bug that reached production, a design decision you had to walk back, not a manufactured near-miss that never actually went wrong.

The "boldly honest" value. Interviewers want to see that you were specific in your disagreement (not just "I had concerns"), that you brought data or an alternative, and that you could adapt when the decision went against you without becoming obstructionist.

The follow-up is almost always: "Looking back, was the decision the right one?" Uber interviewers value intellectual honesty here. If the decision turned out to be wrong and you know it, say so and describe what you learned. If it turned out to be right despite your concerns, that's also interesting and shows intellectual humility.

This is distinct from the product-pushback question above. Interviewers here are probing specifically how you handle disagreement with someone who has authority over your role, your review, and your career trajectory, which changes the social dynamics of pushing back. What they want to hear: you raised the disagreement directly and early rather than complaining to peers, you brought a concrete alternative rather than just objecting, and you executed fully on the final call even if it wasn't yours.

A story where the manager changed their mind based on your input is fine, but a story where they didn't and you still delivered well is often more convincing, since it shows you can commit to a decision you didn't agree with.

The "go get it" and "bias toward action" value. A production incident where you made a call to roll back versus patch forward is the canonical answer structure. Include: what information you had, what you explicitly did not know, what made the decision time-sensitive, and how you communicated it to stakeholders in real time.

Interviewers follow up by asking how you documented the decision and communicated the rationale afterward. Uber's incident culture values written postmortems with explicit decision logs. If you have a postmortem story, that's the right level of detail to bring.

Uber ships across many product surfaces at once (rides, Eats, freight, and more), and requirements genuinely shift mid-project as priorities move. The interviewer wants to hear how you created structure where none existed: did you write down your own interpretation of the requirements and get it confirmed before building, did you build in a way that limited the cost of a requirements change later, rather than just waiting for clarity to arrive.

A weak answer here is "I asked a lot of questions." A stronger one names the specific assumption you made explicit in writing, and what happened when that assumption turned out to be wrong.

Real cross-functional conflicts at Uber usually involve product wanting to ship a feature faster than the engineering team thinks is safe, or data science wanting a model in production that platform engineers believe will create infrastructure problems. Those are the kinds of conflicts interviewers recognize as authentic.

What interviewers are assessing: whether you can represent an engineering perspective clearly to non-engineers, whether you're willing to compromise on timeline to preserve quality, and whether you take partial ownership of the outcome even when the conflict was the other team's call. A story that casts the other team as entirely wrong is a red flag.

Common at the senior IC level. Interviewers want a story where you changed an outcome through the strength of your argument and evidence rather than through your title: getting another team to adopt a shared library, convincing a PM to cut scope, getting a peer team to prioritize a fix that benefited your service more than theirs.

Bring data. (The stories that land best usually have a number in them somewhere, a latency figure, an incident count, a percentage of tickets, something the other side couldn't easily argue with.) A story that's purely about persuasive conversation, with no artifact you built to make your case, tends to feel thin in this round.

Hard questions

5

Push the head node of each of the K lists onto a min-heap keyed by value. Pop the smallest node, append it to the result list, and if that node has a next node, push it back onto the heap. Repeat until the heap is empty. Time complexity is O(N log K), where N is the total number of nodes and K is the number of lists, since every pop and push touches a heap of size at most K.

Reported in Uber phone screens and a couple of onsite loops, usually framed around merging sorted event streams rather than literal linked lists, for example combining trip-completion events arriving on K separate Kafka partitions where each partition is already ordered by timestamp. The interviewer wants the same heap-based merge; the "linked list" framing is just standing in for any set of pre-sorted streams.

Maintain a deque of indices where the corresponding values are in decreasing order. For each new element, pop from the back of the deque while the back's value is less than the current value, then push the current index. Pop from the front if the front index has fallen outside the current window. The front of the deque always holds the index of the maximum for the current window, and each index enters and leaves the deque exactly once, giving O(n) overall despite the nested-looking loops.

Reported from Uber OAs, usually with product framing around "the maximum demand signal in every rolling five-minute window" rather than a bare array. Interviewers sometimes ask candidates to trace the deque state by hand on a small example, which is a good way to catch candidates who memorized the pattern without understanding why decreasing order has to be maintained.

python
from collections import deque

def max_sliding_window(nums, k):
    dq = deque()  # stores indices, values decreasing
    result = []

    for i, num in enumerate(nums):
        while dq and nums[dq[-1]] < num:
            dq.pop()
        dq.append(i)

        if dq[0] <= i - k:
            dq.popleft()

        if i >= k - 1:
            result.append(nums[dq[0]])

    return result

A straightforward merge gets you O(m+n). The interviewer is asking for O(log(min(m,n))) using binary search on the smaller array to find the correct partition point, the point where every element on the left side of both partitions combined is less than or equal to every element on the right side. Once that partition is found, the median comes directly from the four boundary elements without ever fully merging the arrays.

This one is famous enough that Uber interviewers sometimes skip straight past the brute force and ask you to state the binary search invariant before writing a line of code. Get comfortable explaining, out loud, why moving the partition in the smaller array is safe and sufficient. It shows up across interviewing.io mock session writeups tagged Uber going back to at least 2023, and several 2025 Blind threads confirm it is still in rotation for senior backend loops.

Two pointers starting from both ends of the array. Track the running max height seen so far from the left and from the right. At each step, move the pointer on the side with the smaller max height inward, because the water trapped at that position is bounded by the smaller of the two maxes. Add the difference between that max and the current height to a running total. This gets you O(n) time and O(1) space, versus the O(n) space required by a prefix-max/suffix-max array approach.

One of the most frequently LeetCode-tagged "Uber" problems on community trackers going back several years, and it keeps showing up in 2025 and 2026 phone screens. Framed at Uber it sometimes reads as "how much rainwater accumulates between buildings along a delivery route," same array, same algorithm. The follow-up worth preparing for: can you do it without the two auxiliary arrays? Interviewers want to see you reason about why tracking two running maxes is sufficient.

python
def trap(height):
    left, right = 0, len(height) - 1
    left_max, right_max = 0, 0
    water = 0

    while left < right:
        if height[left] < height[right]:
            left_max = max(left_max, height[left])
            water += left_max - height[left]
            left += 1
        else:
            right_max = max(right_max, height[right])
            water += right_max - height[right]
            right -= 1

    return water

Geohash encodes latitude and longitude into a single string by interleaving bits, which makes it trivial to store and index in an ordinary key-value store like Redis. Its weakness is cell shape: geohash cells are rectangles that distort near the poles, and two points a few meters apart on either side of a cell boundary can hash to completely different prefixes, which means proximity searches need to check neighboring cells explicitly or risk missing nearby drivers.

A quadtree recursively subdivides space into four quadrants only where density requires it, so a quiet suburb gets one large cell and a dense downtown gets many small ones. That adapts well to uneven driver density, but quadtrees are harder to shard across a distributed key-value store because cell boundaries shift with the data rather than following a fixed encoding scheme.

Uber's actual answer to this, published as an open-source project, is H3: a hexagonal hierarchical spatial index. Hexagons solve a problem both geohash and quadtree have, every neighbor of a hexagonal cell is equidistant from its center, so "check the 6 neighboring cells" gives a far more uniform search radius than checking the 8 neighbors of a rectangular geohash cell, where diagonal neighbors sit farther away than adjacent ones. Bring up H3 by name in an Uber system design round and explain why the hexagon property matters for neighbor search, and it signals you've actually looked at what the company built rather than defaulting to the generic geohash answer every other candidate gives.

Expect a follow-up on resolution selection. H3 supports 16 resolution levels, and the practical answer is to store driver locations at a fine resolution (roughly 100 to 200 meter cells) and dynamically aggregate to coarser parent cells when a search area has too few active drivers.

Real-time scenario questions

12

The naive design runs a shortest-path algorithm (Dijkstra or A*) over a road-network graph weighted by historical average speeds and returns the result directly. That's a reasonable baseline, but it's systematically wrong in predictable ways: routing engines underestimate ETAs during unusual events (a stadium letting out, an accident) and overestimate them on roads that run faster than history suggests, a highway that was recently widened.

The production-grade answer layers a correction model on top of the routing engine's raw estimate rather than replacing it. The routing engine produces a base ETA from the graph. A separate model, trained on the gap between predicted and actual trip durations, applies a correction factor based on live signals: current traffic density, time of day, weather, and recent actual trip completions on nearby segments. This two-stage design mirrors what Uber has publicly described about its own ETA system, a model layered on top of the routing engine specifically to correct for the routing engine's systematic bias, rather than trying to make the graph search itself traffic-aware in real time.

Interviewers will ask how you'd evaluate this. The metric that matters isn't raw accuracy on individual trips but the calibration of the error distribution, are you consistently off by the same amount in the same direction for a given route type, which tells you the correction model is missing a signal rather than just being noisy.

A rate limiter prevents any single caller from overwhelming downstream services. Uber's API gateway processes hundreds of millions of RPCs per second across thousands of microservices. The design needs to be low-latency (sub-millisecond overhead), distributed (enforced across all gateway instances), and configurable per client and per endpoint.

Algorithm choices: Token Bucket is most commonly used at Uber (Uber's engineering blog describes their token bucket implementation for rate limiting). Fixed Window Counter is simpler but allows burst spikes at window boundaries. Sliding Window Log is the most accurate but memory-intensive at high request rates. Sliding Window Counter approximates the sliding window using two fixed window counters and is a good balance.

Distributed enforcement: each gateway node maintains a local token bucket in memory for fast checks. Every N seconds, nodes sync their consumed token counts with a central Redis store. This means enforcement is approximate across nodes rather than exact, but the overhead of a Redis round-trip per request is unacceptable at Uber's request rate. The approximation is acceptable for most cases; exact enforcement is only needed for payment-critical paths, which get a separate stricter limiter with Redis-based atomic decrements.

Configuration plane: rates are stored in a config service that pushes updates to gateway nodes. This allows per-client, per-endpoint, and per-tier rate limits without a deploy. Uber's actual rate limiting system, described on their engineering blog, uses a combination of per-service limits and per-user limits with a tiered override mechanism.

Trip state transitions (driver assigned, driver arriving, trip started, trip completed) are the events that drive notifications, so the state machine governing a trip's lifecycle is naturally the producer here. Each transition emits an event to a message queue, and a Notification Service consumes those events and decides what to send, to whom, and through which channel.

Channel selection matters more than it sounds. Push is the default, cheap and fast, but it fails silently if the app is backgrounded on certain OS versions or if the user has disabled notifications. SMS is a fallback with real per-message cost, so it should trigger only for time-critical states (driver arrived) and only after a short delay confirms the push wasn't acknowledged, not as a blind duplicate of every push.

Deduplication is the detail that separates a working design from a broken one at scale. If the same trip-state event gets processed twice, because of a consumer retry or a Kafka rebalance, the rider shouldn't get two "your driver has arrived" texts. An idempotency key built from trip ID plus state name, checked against a short-TTL cache before sending, handles this without needing exactly-once delivery guarantees from the queue itself.

A trip goes through a defined lifecycle: REQUESTED, DRIVER_ASSIGNED, DRIVER_EN_ROUTE, ARRIVED_AT_PICKUP, TRIP_IN_PROGRESS, COMPLETED, CANCELLED. Model this as an explicit state machine where each state defines its legal transitions and the events that trigger them. Illegal transitions should throw a domain exception, not silently succeed.

Implementation pattern: an enum of states, a transition map (Map from state to set of valid next states), and a TripStateMachine class that validates and applies transitions. Persist current state to the database with an optimistic lock (version column) to prevent two concurrent events from double-transitioning the same trip. This shows up in Uber's machine coding round as a 45-minute implementation exercise.

java
public enum TripState {
    REQUESTED, DRIVER_ASSIGNED, DRIVER_EN_ROUTE,
    ARRIVED_AT_PICKUP, TRIP_IN_PROGRESS, COMPLETED, CANCELLED
}

public class TripStateMachine {
    private static final Map<TripState, Set<TripState>> TRANSITIONS = Map.of(
        TripState.REQUESTED, Set.of(TripState.DRIVER_ASSIGNED, TripState.CANCELLED),
        TripState.DRIVER_ASSIGNED, Set.of(TripState.DRIVER_EN_ROUTE, TripState.CANCELLED),
        TripState.DRIVER_EN_ROUTE, Set.of(TripState.ARRIVED_AT_PICKUP, TripState.CANCELLED),
        TripState.ARRIVED_AT_PICKUP, Set.of(TripState.TRIP_IN_PROGRESS, TripState.CANCELLED),
        TripState.TRIP_IN_PROGRESS, Set.of(TripState.COMPLETED),
        TripState.COMPLETED, Set.of(),
        TripState.CANCELLED, Set.of()
    );

    public TripState transition(TripState current, TripState next) {
        Set<TripState> validNext = TRANSITIONS.getOrDefault(current, Set.of());
        if (!validNext.contains(next)) {
            throw new IllegalStateException(
                "Invalid transition from " + current + " to " + next
            );
        }
        return next;
    }
}

A single auto-increment column doesn't scale once trip writes are sharded across multiple database instances, and a plain UUID solves uniqueness but loses time-ordering, which matters for anything that needs to scan trips chronologically without an extra index. The standard answer is a Snowflake-style ID: pack a timestamp, a machine or shard identifier, and a per-millisecond sequence counter into a single 64-bit integer.

Bit layout is usually 41 bits for a millisecond timestamp (enough for roughly 69 years from a custom epoch), 10 bits for a machine or shard ID (supports 1,024 generators running concurrently without coordination), and 12 bits for a sequence number that resets every millisecond (4,096 IDs per generator per millisecond before you'd need to wait for the clock to advance). No central coordinator is needed once every generator has a fixed machine ID, which is the whole point, this runs independently on every shard.

python
import time

EPOCH = 1700000000000  # custom epoch in ms

class SnowflakeGenerator:
    def __init__(self, machine_id):
        self.machine_id = machine_id  # 0-1023
        self.sequence = 0
        self.last_timestamp = -1

    def next_id(self):
        timestamp = int(time.time() * 1000)

        if timestamp == self.last_timestamp:
            self.sequence = (self.sequence + 1) & 0xFFF
            if self.sequence == 0:
                while timestamp <= self.last_timestamp:
                    timestamp = int(time.time() * 1000)
        else:
            self.sequence = 0

        self.last_timestamp = timestamp
        return ((timestamp - EPOCH) << 22) | (self.machine_id << 12) | self.sequence

Maintain a max-heap for the lower half of the numbers seen so far and a min-heap for the upper half, keeping the two heaps balanced in size (never differing by more than one element). Inserting a new number means pushing it to the appropriate heap based on a comparison with the max-heap's top, then rebalancing if the size difference exceeds one. The median is either the top of the larger heap, or the average of both tops when the heaps are equal size. Insertion is O(log n); reading the median is O(1).

This comes up in Uber loops with real product framing attached, computing the median ETA across all trips in progress in a city, or the median fare in a surge zone over the last five minutes. One candidate reported the interviewer explicitly asking why a single sorted array wouldn't work here (insertion into a sorted array is O(n), and the two-heap approach beats that for high-frequency streaming inserts).

python
import heapq

class MedianFinder:
    def __init__(self):
        self.small = []  # max-heap (negated values)
        self.large = []  # min-heap

    def add_num(self, num):
        heapq.heappush(self.small, -num)
        heapq.heappush(self.large, -heapq.heappop(self.small))
        if len(self.large) > len(self.small):
            heapq.heappush(self.small, -heapq.heappop(self.large))

    def find_median(self):
        if len(self.small) > len(self.large):
            return -self.small[0]
        return (-self.small[0] + self.large[0]) / 2

Start with functional requirements: rider requests a ride, system finds available drivers within a reasonable radius, matches rider to the best driver, notifies both parties, and tracks the ride through completion. Non-functional: sub-2-second matching latency, 1 million+ concurrent users, 500K+ active drivers globally.

The geospatial matching layer is where interviewers focus most. Encode all driver locations as geohash strings (precision 6 or 7 covers roughly 1 km by 1 km cells). Store in Redis using GEOADD with the driver ID as the member and their coordinates as the value. A GEORADIUS or GEOSEARCH query returns the K nearest drivers in milliseconds without scanning the full driver set. As drivers move, update their Redis entry via WebSocket messages from the driver app through a Location Update Service.

The matching service receives the sorted candidate list from the geospatial layer and applies secondary scoring: driver rating, estimated pickup time accounting for current traffic, vehicle type preference, and whether the driver has opted into surge areas. Dispatch a match request to the top candidate. If the driver declines or doesn't respond within 10 seconds, move to the next candidate.

Data flow: Rider app sends ride request to API Gateway. API Gateway routes to the Ride Request Service, which writes a pending ride record to a PostgreSQL primary. The Matching Service reads driver location data from Redis and queries the Routing Service for ETA estimates. On match acceptance, Kafka emits a RideAccepted event consumed by the Notification Service (FCM/APNS for push), the Trip Tracking Service, and the Payment Service to pre-authorize the card.

Interviewers will probe: "What happens if the Redis node holding driver data fails?" Answer: Redis cluster with replication and sentinel failover, plus a short degradation mode where you fall back to a broader radius search against the replicas. "What if two servers simultaneously match the same driver to two different riders?" Answer: optimistic locking on the driver availability record in Postgres, or a distributed lock via Redlock, with the losing server moving to the next candidate.

Drivers report their GPS coordinates every 3 to 5 seconds. At 500K active drivers updating every 4 seconds on average, that's 125K location writes per second globally. The system needs to serve those positions to riders and dispatchers with low staleness, typically under 5 seconds, and support proximity queries with sub-100ms latency.

Architecture: driver apps maintain a persistent WebSocket connection to a Location Gateway (a stateful service with connection state in memory). The gateway fans location updates into a Kafka topic partitioned by driver ID. A Location Processor consumes from Kafka and writes to Redis Geo. Reads come either directly from Redis (for proximity queries in the matching service) or through a Location Query Service that abstracts the store.

The pub/sub model matters here. Polling from drivers would work but creates thundering herds at scale when many drivers are in one area. Push via persistent WebSocket lets you batch updates and control backpressure from the server side. For the rider app showing driver positions on the map, the server pushes location diffs rather than full coordinates on every tick, reducing bandwidth.

Interviewers push on: "What's your consistency model?" Location data is eventually consistent with a few seconds of acceptable staleness. That's fine for showing a driver moving on a map. It's not fine for billing, where you need to know the exact trip start time and pickup location. Those events go through a strongly consistent write path to Postgres, not through the location stream.

go
// LocationGateway handles WebSocket connections from driver apps
// Each connection pushes location updates into Kafka

type LocationUpdate struct {
    DriverID  string  `json:"driver_id"`
    Lat       float64 `json:"lat"`
    Lng       float64 `json:"lng"`
    Timestamp int64   `json:"timestamp"`
    Speed     float64 `json:"speed_kmh"`
}

func handleDriverConnection(conn *websocket.Conn, producer kafka.Producer) {
    for {
        var update LocationUpdate
        if err := conn.ReadJSON(&update); err != nil {
            // driver disconnected; mark unavailable
            markDriverOffline(update.DriverID)
            return
        }
        // validate: speed plausibility check (>200 km/h = likely GPS spoof)
        if update.Speed > 200 {
            log.Warnf("GPS anomaly for driver %s", update.DriverID)
            continue
        }
        msg := kafka.Message{
            Key:   []byte(update.DriverID),
            Value: mustMarshal(update),
        }
        producer.WriteMessages(context.Background(), msg)
    }
}

Surge pricing adjusts fare multipliers in real time based on the ratio of rider demand to driver supply in a geographic area. The two primary models are rule-based (fixed multipliers when demand/supply exceeds a threshold) and ML-driven (predictive models that factor in traffic, weather, local events, and historical patterns).

The data flow: a Demand Signal Service aggregates open ride requests per geohash cell on a 30-second rolling window. A Supply Signal Service counts available drivers per cell from the location store. A Pricing Engine divides demand count by supply count per cell, applies the multiplier schedule, and writes the current multiplier for each cell to a replicated cache. Fare computation at ride-request time reads from this cache.

Key trade-offs interviewers probe: Cell granularity matters. Geohash precision 5 (4.9 km by 4.9 km cells) is too coarse for dense urban areas. Precision 7 (153m by 153m) may over-fragment supply in low-density areas. A dynamic approach uses precision 6 (1.2 km by 1.2 km) as the default and aggregates to precision 5 when a cell has fewer than 5 active drivers. Boundary effects occur when a hot zone is split across cell boundaries; the fix is to check neighboring cells and apply the highest applicable multiplier.

Fairness concerns surface in behavioral follow-ups. Interviewers sometimes ask: "How do you prevent surge from disproportionately affecting riders in low-income neighborhoods?" This is a real tension Uber has faced regulatory pressure on. A reasonable answer: cap multipliers regionally, offer fixed-price options that lock in the rate at request time, and maintain a maximum surge cap in certain markets per local regulations.

Break the problem into three distinct flows that carry different consistency requirements: fare authorization at ride request, fare capture at trip completion, and driver payout settlement, which is typically batched. Treating all three as one atomic transaction is the mistake that gets flagged in this round; they run on different timescales and different failure domains.

At ride request, pre-authorize (not capture) the estimated fare on the rider's card through a PCI-compliant third-party processor. Card data never touches Uber's own systems directly, only a tokenized reference does, which keeps the compliance scope smaller. At trip completion, the Trip Service emits a final fare amount, and the Payment Service captures the authorized hold, adjusting up or down within the processor's allowed tolerance if the final fare differs from the estimate (tolls, wait time, route changes).

Driver payouts don't happen per trip in real time; they're aggregated into a ledger and settled on a schedule (instant payout is a feature layered on top, not the default path). This is the detail interviewers want to hear reasoned through explicitly: the rider-facing payment path needs to be low latency and immediately confirmable, while the driver-facing ledger needs to be exactly-once and auditable, but doesn't need sub-second latency. An idempotency key on every capture and settlement request, derived from the trip ID, is what prevents a retried network call from double-charging a rider or double-paying a driver.

The follow-up almost every interviewer asks: what happens if the payment capture fails after the rider has already exited the vehicle? The trip still completes from the rider's perspective; the charge goes into a dunning/retry queue with the ride marked settled-pending, and repeated failures escalate to manual collection rather than blocking the rider's app.

This specific scenario was reported in a December 2024 L4 onsite (e-commerce merchandise Uber sells to drivers, food delivery catalog, or similar). Requirements: 50 million items across 10,000 categories, 10K P99 requests per second, popularity-based display, offline batch processing for rankings.

The naive approach builds a real-time ranking query on every request. At 50M items and 10K RPS, that's a query plan that needs significant caching. The right architecture: a batch pipeline runs nightly (or hourly for high-velocity categories), computes popularity scores (views, clicks, purchases over a rolling window), and writes pre-ranked category pages to a read-optimized store. Application servers read from that store, not from the transactional database.

Entity design matters. One candidate reported getting negative feedback for making item categories a nested tree structure in the primary DB with foreign key joins across the tree at query time. The interviewer's concern: category tree joins at 10K RPS don't scale. The right approach is to denormalize: store a flattened category path with each item (e.g., "electronics/phones/iphone") and pre-build category index tables populated by the batch job. Search uses Elasticsearch with pre-indexed popularity scores rather than SQL.

Cache layer: Varnish or CDN at the edge for fully static category pages (high hit rate for top categories), Redis for the popularity-ranked item lists per category (TTL of 5 to 60 minutes depending on category velocity). Invalidation on price changes goes through a write-through pattern to Redis rather than relying on TTL.

Food delivery matching is harder than ride matching because it has three parties instead of two (rider, restaurant, courier) plus an extra dimension of time, the food needs to be ready roughly when the courier arrives, not before and not long after. A naive design that matches a courier the instant an order is placed produces couriers standing around restaurants waiting for food that isn't ready yet.

The dispatch logic needs a food-prep-time estimate, pulled from the restaurant's historical average prep time for similar orders and adjusted by current order volume at that location. Matching happens in two stages: an early "soft" assignment that reserves a courier candidate without committing, and a "hard" dispatch that fires once the estimated food-ready time falls within the courier's travel time to the restaurant. Get this wrong in either direction and you either have idle couriers burning time at the restaurant or a completed order going cold on a counter.

Batching is the other axis interviewers probe. A single courier picking up two or three orders from restaurants near each other is more efficient than one courier per order, but only if the routing doesn't push any single order's delivery time past an acceptable threshold. The scoring function for a candidate courier needs to account for whether adding this order to an existing batch keeps every order in that batch within its delivery SLA, not just whether the courier happens to be geographically close.

What we've seen across Uber loops

Candidates who practice Uber interview prep through LastRoundAI's mock sessions show a consistent failure pattern that standard prep guides don't capture. The system design round ends most Uber loops prematurely, and the reason is almost never that the candidate doesn't know the algorithm. It's that they design for a generic internet company rather than for a ride-hailing marketplace.

Specifically: candidates who open with "I'd put the data in MySQL and add Redis caching" without naming geohash, without discussing eventual versus strong consistency by data type, and without addressing the supply-demand ratio as a first-class system concept tend to score below the bar even when their overall architecture is sound. Uber interviewers have worked with these systems. They know what the real constraints feel like.

The second pattern we see: Uber's behavioral round carries more weight than candidates expect. The "collaboration and leadership" round run by the hiring manager is not a warmup. Interviewers push on specific numbers ("what was the P99 latency after your change?"), specific team conflicts ("what exactly did you disagree on, and how was it resolved?"), and specific timelines. Vague STAR answers without concrete data points don't land. Prepare your two or three strongest stories with metrics the same way you'd prepare a system design diagram.

Leave a Reply

Your email address will not be published. Required fields are marked *