DoorDash Interview Questions · 2026

DoorDash Interview Questions (2026): 50 Real, Answered

A candidate on Blind described his DoorDash phone screen this way: his code passed every test case on the first try, and he still got rejected. That story shows up often enough in DoorDash interview threads that it's worth saying upfront, this is not a company that grades you purely on whether the tests go green. DoorDash's own engineers describe the loop as heavy on "code craft," an open-ended object-oriented design exercise where the interviewer cares as much about your class boundaries and naming as your correctness. Glassdoor lists 209 questions and 214 reviews for the Software Engineer track alone, with candidates rating the process 3.1 out of 5 for difficulty and reporting an average 24 days from first contact to offer (Glassdoor, DoorDash Software Engineer interviews).

The pattern across Glassdoor, Blind, and 1Point3Acres threads is consistent: an online assessment on HackerRank with two logic-heavy problems, a technical phone screen that leans on debugging and object-oriented design more than raw algorithms, then a virtual onsite loop of three to four sessions covering practical coding, system design, a domain deep-dive, and behavioral. System design questions stay close to the product, dispatch and matching, real-time Dasher location, ETA estimation, regional restaurant search, rather than generic "design Twitter" prompts. Non-engineering loops, strategy and operations, data science, and product, run their own case-style rounds built around DoorDash's three-sided marketplace of consumers, merchants, and Dashers.

This page walks through six rounds in the order most candidates actually see them: the online assessment, the core coding interview, system design, the domain and practical-engineering round, behavioral, and the strategy/operations and data case questions that non-eng candidates get instead. Every question below is paraphrased from what candidates report having been asked, not invented, and every answer is original.

~50Questions
5Rounds
24 daysAvg Time to Hire
3.1/5SWE Difficulty Rating

Round 1: The online assessment (HackerRank)

Candidates consistently describe the DoorDash OA as two problems in roughly 90 minutes, both landing at medium-to-hard difficulty. The emphasis is logic simulation and data-structure-driven optimization over textbook algorithms, and the problem statements tend to be dense with rules you have to track carefully rather than short and elegant.

Easy questions

17

Use two pointers moving from the right. Walk the array from the end, and every time you hit a non-zero value, place it at the current write position (also starting from the end) and decrement that write pointer. Once you've placed every non-zero element, fill everything before the write pointer with zeros.

python
def move_zeros_left(arr):
  write = len(arr) - 1
  for i in range(len(arr) - 1, -1, -1):
    if arr[i] != 0:
      arr[write] = arr[i]
      write -= 1
  for i in range(write, -1, -1):
    arr[i] = 0
  return arr

This runs in one pass with O(1) extra space, which is the detail interviewers actually check for. A version that builds a new list and reassigns it will pass the tests but usually costs you points on the follow-up about memory.

Sort the array first, then fix the smallest index of each triplet and use two pointers on the remainder. For a fixed left index, move a low pointer just after it and a high pointer at the end; if the three values sum below the target, every index between low and high also works with the current low held fixed, so you can add (high - low) to your count in one step and move low forward. If the sum is too big, move high back.

python
def count_triplets_below(nums, target):
  nums.sort()
  count = 0
  n = len(nums)
  for i in range(n - 2):
    lo, hi = i + 1, n - 1
    while lo < hi:
      if nums[i] + nums[lo] + nums[hi] < target:
        count += hi - lo
        lo += 1
      else:
        hi -= 1
  return count

The brute force triple loop is O(n cubed) and will time out on the larger inputs DoorDash tends to use. Sorting plus two pointers gets this down to O(n squared), which is the answer they're actually looking for.

You don't need to solve the puzzle, just validate the current state. Walk every filled cell once and track three sets, seen values per row, per column, and per 3x3 box (computed as row/3, col/3). If a value you've already recorded in any of those three sets shows up again, the board is invalid.

python
def is_valid_sudoku(board):
  rows, cols, boxes = [set() for _ in range(9)], [set() for _ in range(9)], [set() for _ in range(9)]
  for r in range(9):
    for c in range(9):
      val = board[r][c]
      if val == '.':
        continue
      b = (r // 3) * 3 + c // 3
      if val in rows[r] or val in cols[c] or val in boxes[b]:
        return False
      rows[r].add(val)
      cols[c].add(val)
      boxes[b].add(val)
  return True

A single pass with a hash map beats the O(n squared) brute force. Walk the array once, and before adding the current number to the map, check whether target minus the current number is already a key. If it is, you've found your pair immediately, no need to store every complement upfront and scan twice.

python
def two_sum(nums, target):
  seen = {}
  for i, n in enumerate(nums):
    complement = target - n
    if complement in seen:
      return [seen[complement], i]
    seen[n] = i

A stack solves this directly. Push every opening bracket. On a closing bracket, pop the stack and check that it matches the expected opening type; if the stack is empty or the popped value doesn't match, the string is invalid. At the end, the stack must be empty, otherwise there are unmatched opens left over.

python
def is_valid(s):
  pairs = {')': '(', ']': '[', '}': '{'}
  stack = []
  for ch in s:
    if ch in pairs.values():
      stack.append(ch)
    elif ch in pairs:
      if not stack or stack.pop() != pairs[ch]:
        return False
  return not stack

Walk the list once with three pointers, previous, current, and next. At each node, save the next pointer before you overwrite it, flip the current node's pointer to point backward at previous, then advance both previous and current forward by one.

python
def reverse_list(head):
  prev = None
  curr = head
  while curr:
    nxt = curr.next
    curr.next = prev
    prev = curr
    curr = nxt
  return prev

Two passes with a hash map, one to build a count of every character, a second to walk the string in original order and return the first character whose count is exactly 1. Two passes over the string is still O(n) overall, and it's simpler to reason about than trying to track "first unique" in a single pass with an ordered structure.

python
def first_unique_char(s):
  counts = {}
  for ch in s:
    counts[ch] = counts.get(ch, 0) + 1
  for ch in s:
    if counts[ch] == 1:
      return ch
  return None

Keep a second stack alongside the main one that tracks the running minimum. Every time you push a value onto the main stack, push the smaller of that value and the current top of the min-stack onto the min-stack too, so the min-stack's top is always the minimum of everything currently in the main stack. Popping from one pops from both, keeping them in lockstep.

python
class MinStack:
  def __init__(self):
    self.stack = []
    self.min_stack = []

  def push(self, val):
    self.stack.append(val)
    m = val if not self.min_stack else min(val, self.min_stack[-1])
    self.min_stack.append(m)

  def pop(self):
    self.min_stack.pop()
    return self.stack.pop()

  def get_min(self):
    return self.min_stack[-1]

Start with the simplest version: an order service owns a status field (placed, confirmed, preparing, picked up, delivered) and a status_history table with timestamps for each transition. A GET endpoint keyed by order ID returns the current status plus the history. That alone gets you 80 percent of the value with no real-time infrastructure.

The interesting part is what happens when the customer wants updates pushed to them instead of polling. That's where you'd bring in either short-interval polling from the client (fine at low scale, wasteful at DoorDash's volume) or a push mechanism, websockets or server-sent events backed by a pub/sub layer that the order service publishes status changes into. I'd start with polling to prove out the data model, then justify the push layer once polling load becomes the actual bottleneck, not before.

A basic health check answers one question: can this service accept and process a request right now. The lightest version returns 200 with no dependency checks at all, useful for a load balancer deciding whether to route traffic to an instance. A deeper version, sometimes split into a separate "readiness" check, also verifies that the service's own hard dependencies, its database connection, a required downstream API, actually respond, since a process that's technically running but can't reach its database isn't really healthy.

The design mistake to avoid is a health check that itself calls slow, expensive downstream services on every ping. If the health check takes 2 seconds because it's fanning out to five dependencies, a load balancer polling it every few seconds can create real load on those dependencies just from health checks alone. I'd keep the liveness check nearly free and let a separate, less frequent readiness check own the deeper dependency verification.

Menus change rarely, maybe a few times a week per restaurant, but get read constantly by every browsing customer. That read/write ratio is the whole design: put a cache in front of the database, keyed by restaurant ID, with a TTL long enough to absorb most traffic and a write-through or explicit invalidation on menu updates so stale prices don't linger.

The database itself can be a straightforward relational table, restaurant_id, item_id, name, price, availability, since menu data is structured and doesn't need the flexibility of a document store. The cache is doing almost all the work here, and I'd size it in-memory (Redis or similar) rather than reaching for anything more exotic for what's fundamentally a read-heavy lookup problem.

Start with a shared base, an Order with an ID, customer, items, and status, and let the fulfillment-specific behavior live in a strategy object rather than a chain of if-statements checking order type everywhere. A RestaurantFulfillment strategy knows about prep time and a single pickup location; a DashMartFulfillment strategy knows about warehouse picking and might support multiple items being staged in parallel.

python
class Order:
  def __init__(self, order_id, items, fulfillment):
    self.order_id = order_id
    self.items = items
    self.fulfillment = fulfillment # strategy object

  def estimated_ready_time(self):
    return self.fulfillment.estimate_ready(self.items)

class RestaurantFulfillment:
  def estimate_ready(self, items):
    return sum(i.prep_minutes for i in items)

class DashMartFulfillment:
  def estimate_ready(self, items):
    return max(i.pick_minutes for i in items)

The interviewer is watching whether new fulfillment types can be added without touching the Order class itself, that's the actual design goal, not just "does it compile."

Start by narrowing "intermittent" to something concrete: pull a sample of affected orders and look for what they share, same payment processor, same retry pattern in the logs, same geographic region, same time window. Intermittent almost always means there's a race condition or a retry-without-idempotency bug hiding, not a deterministic logic error, since a deterministic bug would reproduce every time.

Once I have a shared trait across the affected orders, I'd trace one specific order end to end through the logs, request ID by request ID, looking for two charge calls with the same order ID close together in time. If I find that, the fix is almost always adding or fixing an idempotency key on the charge call, so a retry recognizes "I already did this" instead of creating a second charge.

Pick a real disagreement with a concrete stake, a technical direction, a prioritization call, not a manufactured one where you were obviously right and your manager obviously wrong. Walk through how you made your case with evidence rather than just asserting your opinion louder, what you did once a decision was made even if it didn't go your way, and what actually happened afterward. DoorDash interviewers are listening for whether you can disagree without becoming unproductive once the call is made, since "I was right and refused to let it go" is a worse answer than "I made my case, lost, and executed the decision fully anyway."

Name the actual source of the conflict specifically, a technical disagreement, overlapping ownership of the same piece of work, a missed handoff, rather than describing it vaguely as "personality differences." Then walk through the concrete step you took to resolve it, a direct conversation, a clarified ownership boundary, a decision escalated to someone with the authority to break the tie, and what changed afterward as a result. The weakest version of this answer ends at "we talked it out and things got better" with no specifics about what was actually said or decided.

Candidates report getting this question directly, and it rewards specificity over a general statement of values. A strong answer describes one concrete action, advocating for a hiring process change that removed a biased screening step, making sure a quieter team member's technical proposal actually got heard in a meeting dominated by louder voices, rather than a broad claim like "I always value diverse perspectives" with nothing underneath it. Tie the story to an outcome you can point to, not just an intention.

This is a Fermi estimate, and the interviewer cares about your structure and stated assumptions far more than the final number. Start from population, say a metro of 500,000 people, estimate what share order delivery on a given Friday night (maybe 5 to 8 percent, since Friday is a peak night), and multiply by an average order count per orderer (usually close to 1). That gets you somewhere in the 25,000 to 40,000 delivery range for that city that night.

State each assumption out loud as you make it, population, penetration rate, orders per orderer, so the interviewer can push back on any single number without you having to redo the whole calculation. The specific final number matters far less than whether your assumptions are reasonable and internally consistent.

Medium questions

18

This is one of the more frequently reported OA and phone-screen questions, and it's a straight max-heap problem dressed up in DoorDash's domain. Compute squared Euclidean distance for each restaurant (skip the square root, it's monotonic and you don't need the actual value), and maintain a max-heap of size K. Push a distance in; if the heap exceeds K, pop the largest.

python
import heapq

def k_nearest_restaurants(restaurants, customer, k):
  heap = []
  for r in restaurants:
    dist = (r.x - customer.x) ** 2 + (r.y - customer.y) ** 2
    heapq.heappush(heap, (-dist, r.id))
    if len(heap) > k:
      heapq.heappop(heap)
  return [rid for _, rid in heap]

This runs in O(n log k), which beats sorting the full list when k is small relative to the number of restaurants in a market. Interviewers usually follow up asking what changes if restaurants and customers move in real time, at which point the honest answer is a spatial index like a grid or a k-d tree, since recomputing distance to every restaurant on every request doesn't scale to a live map.

Strip away the delivery framing and this is interval scheduling maximization. Sort orders by drop-off time (the end of the window), then greedily take an order if its pickup time is at or after the drop-off time of the last order you accepted. Greedy works here because always freeing up the Dasher as early as possible never hurts your ability to fit more orders later, which is the standard exchange-argument proof for this class of problem.

python
def max_orders(orders):
  orders.sort(key=lambda o: o.dropoff)
  count, last_end = 0, float('-inf')
  for o in orders:
    if o.pickup >= last_end:
      count += 1
      last_end = o.dropoff
  return count

The follow-up that trips people up: what if the Dasher can carry more than one order at once, up to a bag capacity, as long as the pickup locations are close together. That turns a clean greedy problem into something closer to bin packing, and the honest answer is that it stops being solvable optimally in polynomial time and you'd reach for a heuristic instead.

Sort by start time, then walk through and compare each interval to the last one you've kept. If the current interval's start is at or before the last kept interval's end, they overlap, so extend the last interval's end to the max of the two. Otherwise, the current interval starts a new merged group.

python
def merge_windows(windows):
  windows.sort(key=lambda w: w[0])
  merged = [windows[0]]
  for start, end in windows[1:]:
    last_start, last_end = merged[-1]
    if start <= last_end:
      merged[-1] = (last_start, max(last_end, end))
    else:
      merged.append((start, end))
  return merged

This comes up in DoorDash interviews framed around consolidating overlapping delivery or pickup windows for batching, but it's the exact same interval-merging pattern you'd use for calendar meetings. Recognizing that the domain wrapper doesn't change the algorithm is most of the battle.

The key insight is that anagrams share the same sorted-character signature. Build a hash map keyed by that signature, either the sorted string itself or a tuple of 26 letter counts, and append each word to the bucket for its signature. Every bucket at the end is one group of anagrams.

python
from collections import defaultdict

def group_anagrams(words):
  groups = defaultdict(list)
  for w in words:
    key = ''.join(sorted(w))
    groups[key].append(w)
  return list(groups.values())

Sorting each word costs O(k log k) for a word of length k, so total time is O(n * k log k) across n words. Using a 26-length count tuple as the key instead of a sorted string drops that to O(n * k), which is the answer to give if the interviewer asks how to optimize further.

Sliding window with a hash map tracking the last seen index of each character. Expand the window's right edge one character at a time; if that character was already seen inside the current window, jump the left edge forward to just past its previous occurrence instead of resetting the whole window. Track the max window size as you go.

python
def longest_unique_substring(s):
  last_seen = {}
  left = 0
  best = 0
  for right, ch in enumerate(s):
    if ch in last_seen and last_seen[ch] >= left:
      left = last_seen[ch] + 1
    last_seen[ch] = right
    best = max(best, right - left + 1)
  return best

This is topological sort, and DoorDash frames it around order fulfillment steps, prep before pickup before delivery, but the mechanics are the same as course scheduling. Build an adjacency list and an in-degree count for each node, seed a queue with every node that has in-degree zero, then repeatedly pop a node, add it to the result, and decrement the in-degree of its neighbors, pushing any that hit zero. If the result doesn't include every node at the end, there's a cycle and no valid order exists.

python
from collections import deque, defaultdict

def topo_order(num_tasks, edges):
  graph = defaultdict(list)
  in_degree = [0] * num_tasks
  for a, b in edges:
    graph[a].append(b)
    in_degree[b] += 1
  queue = deque([i for i in range(num_tasks) if in_degree[i] == 0])
  order = []
  while queue:
    node = queue.popleft()
    order.append(node)
    for nxt in graph[node]:
      in_degree[nxt] -= 1
      if in_degree[nxt] == 0:
        queue.append(nxt)
  return order if len(order) == num_tasks else []

Track the timestamp of the last ping per Dasher, and treat a Dasher as offline once the gap since that last ping exceeds a threshold, say 60 to 90 seconds given a normal ping interval of a few seconds. Run this as a background sweep on a timer rather than trying to react instantly to a missing ping, since "missing" only becomes meaningful after a threshold has passed, not the instant a single expected ping doesn't arrive.

python
def sweep_offline_dashers(last_ping_by_dasher, now, timeout_seconds=90):
  offline = []
  for dasher_id, last_ts in last_ping_by_dasher.items():
    if now - last_ts > timeout_seconds:
      offline.append(dasher_id)
  return offline

The real-world wrinkle worth raising unprompted: a Dasher going through a tunnel or losing signal for 30 seconds looks identical to actually going offline, so you don't want to immediately reassign their active order the moment the timeout trips. A grace period before reassignment, plus a distinction between "no pings" and "app force-closed," keeps you from yanking an order away from someone who just lost signal for a moment.

Give every event a monotonically increasing sequence number or timestamp scoped to its order, and track the last-applied sequence number per order in whatever service consumes the events. When an event arrives, compare its sequence number to the last one applied for that order ID; if it's less than or equal, it's a duplicate or a stale out-of-order event and you drop it, if it's exactly one more than expected you apply it, and if it skips ahead you either buffer it briefly waiting for the gap to fill or explicitly flag a missing event for investigation.

python
def should_apply_event(last_applied_seq, incoming_seq):
  return incoming_seq > last_applied_seq

The subtlety worth mentioning is that "drop it silently" is fine for a true duplicate but dangerous for a gap, silently ignoring a skipped sequence number can leave an order stuck in a stale status forever with no alert. I'd log every dropped or gapped event even when the immediate handling is "do nothing," so there's a trail to debug from later.

One candidate reported exactly this, a technically correct solution that still got dinged on naming choices, and the honest answer is that at DoorDash's code-craft rounds, readability is graded as a real signal, not a nitpick. The right response in the moment is to just fix it without getting defensive, rename the variable, briefly explain why the new name is clearer, and move on. Arguing that "it doesn't affect correctness" misses the point of the round entirely, since the round exists specifically to see how you write code other people will maintain.

If I'm prepping for this round specifically, I practice narrating my naming choices out loud as I write, not just picking a name silently, since interviewers evaluating code-craft are listening for whether you're thinking about the next reader at all, not just whether you eventually land on a good name.

Generate an idempotency key on the client side before the first attempt, a UUID tied to that specific checkout, and send it as a header on every attempt to charge, including retries. The payment service stores that key alongside the result of the first successful call, and if a retry arrives with a key it's already seen, it returns the stored result instead of processing a second charge.

python
def charge_with_idempotency(payment_client, idempotency_key, amount):
  existing = payment_client.lookup_by_idempotency_key(idempotency_key)
  if existing:
    return existing
  return payment_client.charge(amount, idempotency_key=idempotency_key)

The detail worth calling out unprompted: the idempotency key has to be generated once and reused across retries of the same logical attempt, not regenerated fresh on every retry. Regenerating it defeats the entire mechanism, since a new key on each attempt looks like a brand new charge to the payment service every single time.

This question is testing whether you can own a mistake made under DoorDash's actual bias toward speed, not whether you've never made one. Describe the decision to move fast honestly, including what you knew and didn't check at the time, what broke as a direct result, and what you personally did once it broke, not just what the team did collectively. The strongest answers include a concrete change you made afterward, a check you now run before shipping something similar, that shows the lesson stuck rather than just being a nice thing to say in an interview.

Pick a case where pushing back had real stakes, a deadline you argued should move, a scope you argued should shrink, not a low-stakes example where the stakeholder immediately agreed with you. Walk through the reasoning you used to make your case, ideally backed by a specific number or piece of data rather than a gut feeling, and be honest if the outcome was a partial win or an outright loss. DoorDash's interviewers are specifically listening for whether you lean on data to make your case, since that's called out as a core trait they screen for.

Cross-functional influence without formal authority is a recurring theme in DoorDash's behavioral round, likely because the actual job involves constantly coordinating across engineering, ops, and other functions with no direct reporting line between them. Describe specifically how you built buy-in, one-on-one conversations before a group meeting, a small proof of concept that made the idea concrete instead of abstract, data that made the case for you before you had to argue it verbally. Vague answers like "I just communicated well" don't hold up under a follow-up asking exactly what you said to the person who was hardest to convince.

Choose a failure that actually cost something, a missed deadline, a bug that reached production, a decision that had to be reversed, not a disguised humblebrag like "I worked too hard." Be precise about your own role in what went wrong, since a story where every mistake belongs to someone else doesn't read as genuine reflection. Close with a specific change to how you work now, not a generic lesson like "communication is important," since the specificity of the fix is what signals you actually internalized it.

Start by isolating whether the change is on the restaurant side, the Dasher side, or the order mix. Pull the same metric segmented by individual restaurant in that zone, if the increase concentrates in one or two restaurants rather than spreading evenly, that points to a specific kitchen's prep capacity or staffing, not a zone-wide problem. If it's spread evenly across restaurants, check whether order volume in the zone rose without a matching increase in restaurant staffing or Dasher supply, which would point to demand simply outpacing capacity.

I'd also check the composition of orders, has average order size or item complexity increased, since a shift toward larger, more complex orders naturally increases prep time even with no change in restaurant efficiency. The answer that treats this as a single root cause is usually wrong, DoorDash's marketplace problems are almost always a mix of a demand-side and a supply-side effect happening at once, and the diagnosis should say so rather than pick one story.

Grocery breaks assumptions baked into restaurant delivery. Order fulfillment time is longer and more variable, since a Dasher is now shopping inside a store rather than waiting for a bag handed across a counter, which changes both the ETA model and the pay structure that made sense for restaurant orders. Basket substitutions become a real problem too, an item is out of stock and someone, the Dasher or an automated system, has to decide on a substitute in real time, which restaurant delivery never has to solve.

Cold chain and item damage are also new failure modes, groceries include perishables that restaurant orders don't, so packaging, temperature, and bag capacity constraints all need rethinking. I'd flag that grocery is a genuinely different operating model wearing the same delivery-app skin, and treating it as "restaurant delivery but with more items" is the mistake most likely to cause real launch problems.

Run this as a geo-split A/B test rather than a national rollout from day one, holding out comparable markets as a control so you can isolate the pay plan's effect from normal seasonal or market-specific swings. The primary metric should be earnings per active hour for Dashers, since that's the number the plan is actually trying to move, alongside secondary metrics like Dasher retention, acceptance rate on offered orders, and total delivery cost per order for DoorDash's side of the ledger.

Before calling it a win, I'd check for a specific failure mode this exact structure invites, Dashers gaming the "every fifth delivery" bonus by cherry-picking short, fast deliveries to hit the count quickly rather than accepting the deliveries that actually need coverage, which would show up as a drop in acceptance rate on longer or less convenient orders even while the headline earnings number looks good.

Run it as a controlled experiment, holding a control group that never sees the estimate against a treatment group that does, randomized at the user or session level rather than by market, so you're not confounding the change with normal regional variation. The primary metric is checkout completion rate for sessions that reach the point where the estimate would show, with order value and post-order cancellation rate as guardrails, since a wait-time estimate that scares away high-value orders while barely helping completion overall would be a false win on the headline number.

I'd also segment the result by how long the estimate actually was. A wait-time estimate probably increases completion when it's short and reassuring, and could just as easily decrease it when it's long and honest, so an aggregate lift across every session can hide the fact that the feature helps some orders and hurts others. Segmenting by estimate length before declaring a global win avoids shipping a change that trades away your highest-friction, highest-value orders for a better-looking average.

Hard questions

8

Candidates on this OA report this exact shape of problem, and the difficulty isn't the algorithm, it's reading the rules precisely enough not to silently violate one of them. You maintain a live window with two pointers, left and right, over the remaining elements. Each round: find the max among the first k elements in the window and the max among the last k, compare those two candidates, and on a tie prefer the one at the smaller absolute index. Remove that element (shift the window boundary past it, or mark it removed if you're using an index array), then repeat until the window is exhausted.

python
def team_formation(nums, k):
  remaining = list(range(len(nums)))
  removed_order = []
  while remaining:
    left_k = remaining[:k]
    right_k = remaining[-k:]
    best_left = max(left_k, key=lambda i: nums[i])
    best_right = max(right_k, key=lambda i: nums[i])
    if nums[best_left] >= nums[best_right]:
      chosen = best_left
    else:
      chosen = best_right
    removed_order.append(chosen)
    remaining.remove(chosen)
  return removed_order

That implementation is deliberately the naive O(n squared) version, using a plain list remove, because getting a correct answer under time pressure matters more here than a perfectly tuned data structure. The real interview signal is whether you re-derive the tie-break rule correctly after the first removal shifts every index, since that's exactly where people who skim the prompt lose points.

Model every word as a node and connect two words if they differ by exactly one letter, then run breadth-first search from the start word, since BFS guarantees the first time you reach the end word is via a shortest path. The expensive part is generating neighbors efficiently: instead of comparing every word pair directly, which is O(n squared * k), generate all single-letter-changed variants of the current word (k positions times 25 alternate letters) and check each against a set of the dictionary.

python
from collections import deque
import string

def word_ladder_length(begin, end, word_list):
  words = set(word_list)
  if end not in words:
    return 0
  queue = deque([(begin, 1)])
  while queue:
    word, steps = queue.popleft()
    if word == end:
      return steps
    for i in range(len(word)):
      for c in string.ascii_lowercase:
        candidate = word[:i] + c + word[i+1:]
        if candidate in words:
          words.remove(candidate)
          queue.append((candidate, steps + 1))
  return 0

Removing a word from the set the moment it's queued is what keeps this from revisiting the same node repeatedly and turning into an exponential search. One candidate on Blind reported getting an LC Hard dynamic programming problem in this slot instead, so it's worth practicing at least one DP problem, edit distance or a knapsack variant, at this difficulty too, since the exact question varies more here than in the OA.

This maps to the debugging rounds candidates describe on Blind and Glassdoor, where the interviewer hands you code that mostly works and expects you to reason out loud rather than silently stare at it. Start by asking what "double-charges" looks like concretely, is it a duplicate row in a payments table, two calls to a charge function, or a retry that doesn't check idempotency, since the fix is completely different depending on which one it is.

Once you've isolated the failure to a specific branch, narrate your hypothesis before you change anything: "if this loop runs one extra iteration when the list is empty, that would explain a charge firing twice on a zero-item edge case." Then trace through a concrete example by hand rather than jumping straight to a fix. Interviewers running this round are grading your process as much as the final patch, jumping straight to a guess-and-check edit reads worse than a slower, correct diagnosis.

Every transition, confirmed, in prep, picked up, delivered, should be an idempotent operation identified by a unique event ID, so if the notification service or the payment service retries a call because it didn't get a response in time, replaying the same transition twice doesn't move the order backward or double-fire a side effect like a charge or a push notification. I'd back the order state with an event log, not just a mutable current-state row, so any consumer that missed an update during an outage can replay from the log once it recovers, instead of silently drifting out of sync with the source of truth.

For the outage itself, the honest design decision is picking, per downstream dependency, whether a failure should block the transition (payment must succeed before you mark an order confirmed) or degrade gracefully (a notification failure shouldn't block the order from moving to picked up, it should just retry the notification separately). Treating every dependency as equally critical is the mistake that turns one service's bad day into an outage for the entire order pipeline.

The safest fix is a conditional write at the database level, an update that only succeeds if the order's current status matches what you expect it to be before the transition, using something like an optimistic concurrency check (a version number or a WHERE clause on the expected prior status) rather than a plain unconditional UPDATE. Whichever write loses the race gets zero rows affected back from the database and knows to treat its own attempt as a no-op rather than blindly assuming it succeeded.

sql
UPDATE orders
SET status = 'picked_up', version = version + 1
WHERE order_id = :id AND status = 'confirmed' AND version = :expected_version;
-- if rows_affected == 0, this writer lost the race, don't proceed as if it succeeded

A distributed lock keyed by order ID is the alternative if you can't rely on a single database enforcing the check, but I'd reach for the conditional-write pattern first since it doesn't introduce a separate coordination system with its own failure modes. Either way, the wrong answer is "just let both writes happen, last write wins," since that silently drops information about which service actually observed the pickup first.

This is the deepest version of the failure question, and it's designed to see whether you'll actually surface the unflattering details or sand them down into something safe. A strong answer names the exact moment you realized you'd made the wrong call, what it would have cost to hide or minimize that realization, and what you did instead, escalating early, over-communicating the bad news, rebuilding trust with whoever was affected. The measurable outcome matters here too: what changed afterward that you can point to concretely, a new check in your process, a changed relationship with a stakeholder, not just a vague sense that you "grew from it."

Interviewers at this level are also listening for whether the story reflects genuine ownership of the marketplace tradeoffs DoorDash cares about, did the failure hurt Dashers, merchants, or customers specifically, and did you weigh that when deciding how urgently to fix it, or did you treat it as purely an internal engineering problem.

The scoring logic should differ by market because the underlying supply and demand dynamics differ. In a dense market like New York, the constraint is usually speed of coverage in specific high-order neighborhoods rather than total Dasher count, so I'd weight recruiting toward zip codes with the highest projected order density and build a scoring model per candidate Dasher that favors proximity to those zones and historical reliability signals from similar gig work if available. In a lower-density market like Charlotte, the constraint shifts toward simply having enough total coverage across a wider geographic area, since demand is spread more thinly, so the scoring model should weight availability and willingness to cover a larger radius more heavily than hyper-local density.

Either way, I'd build the launch plan around a small number of measurable inputs, historical on-time rates if the candidate has prior gig delivery experience, self-reported availability during projected peak hours, and geographic coverage relative to where early restaurant partners are concentrated, rather than a single blended national score that ignores how differently these two markets actually behave.

Frame it explicitly as a three-sided trade-off rather than a single metric to optimize. Dispatching a Dasher earlier reduces the restaurant's idle wait for a Dasher to show up, but if it's too early, the Dasher now waits at the restaurant for the food to be ready, and if the ETA shown to the customer was based on the earlier dispatch time, the customer's actual wait can grow even though the "assign a Dasher" step got faster. The metric that matters is total order-to-doorstep time for the customer, not any single intermediate step in isolation, since optimizing one leg of the journey while ignoring its effect on the rest is exactly how a well-intentioned change makes the end-to-end experience worse.

I'd want to see the dispatch-timing decision modeled against restaurant-specific prep time distributions, not a single global dispatch offset applied everywhere, since a fast-prep restaurant and a slow-prep restaurant need genuinely different early-dispatch windows to hit the same target of near-zero Dasher wait without pushing customer wait up. Rolling this out as a blanket policy change without that restaurant-level tuning is the version of this trade-off most likely to look good in aggregate while quietly making specific restaurants worse.

Real-time scenario questions

7

Combine a hash map with a doubly linked list. The hash map gives O(1) lookup from key to a node in the list; the linked list gives O(1) removal and O(1) insertion at either end, which is what lets you move an entry to the "most recently used" position without shifting anything else. On get, move the accessed node to the front. On put, insert at the front and evict from the back if you're over capacity, updating the hash map on both operations.

python
class Node:
  def __init__(self, key, val):
    self.key, self.val = key, val
    self.prev = self.next = None

class LRUCache:
  def __init__(self, capacity):
    self.cap = capacity
    self.map = {}
    self.head = Node(0, 0)
    self.tail = Node(0, 0)
    self.head.next = self.tail
    self.tail.prev = self.head

  def _remove(self, node):
    node.prev.next, node.next.prev = node.next, node.prev

  def _add_front(self, node):
    node.next = self.head.next
    node.prev = self.head
    self.head.next.prev = node
    self.head.next = node

  def get(self, key):
    if key not in self.map:
      return -1
    node = self.map[key]
    self._remove(node)
    self._add_front(node)
    return node.val

  def put(self, key, value):
    if key in self.map:
      self._remove(self.map[key])
    node = Node(key, value)
    self.map[key] = node
    self._add_front(node)
    if len(self.map) > self.cap:
      lru = self.tail.prev
      self._remove(lru)
      del self.map[lru.key]

Interviewers tend to use this to check whether you actually understand why a plain dictionary with an "ordered by access" list doesn't give you O(1) reordering, since moving an item in a Python list is O(n).

The Dasher's app sends a location ping every few seconds over a lightweight connection, ideally a persistent one like a websocket or gRPC stream rather than a fresh HTTP request each time, since re-establishing a connection every few seconds adds meaningful overhead at DoorDash's scale. Those pings land on a location service that writes the latest position to a fast key-value store keyed by delivery ID, not a full relational table, since you only ever care about the most recent point, not a full history, for the live-tracking use case.

On the read side, the customer's app subscribes to updates for their specific delivery ID through a pub/sub channel, so the location service publishes each new ping to that channel and every subscribed client gets it pushed, instead of every customer's app polling the database directly. Sharding the pub/sub layer by geography or by delivery ID range keeps any single node from being flooded by every active delivery nationwide. I'd flag battery drain and cellular reliability on the Dasher's phone as a real constraint too, so the ping interval and retry behavior need to degrade gracefully on a spotty connection, not assume a perfect network.

At the core this is an assignment problem: given a set of available Dashers and a set of open orders, pick pairings that minimize some cost, typically a blend of distance, expected delivery time, and Dasher idle time, while respecting constraints like a Dasher's current bag capacity. A naive greedy match, assign the nearest available Dasher to each new order as it arrives, is a reasonable starting design and is probably close to what a real system does for the common case.

Where it gets harder is batching decisions, should this order wait 90 seconds to see if a second order from a nearby restaurant shows up, so one Dasher can carry both. That trade-off between speed (assign immediately) and efficiency (batch and wait a little) is exactly the kind of tension I'd expect an interviewer to push on, and there's no universally right answer, it's a live experiment DoorDash actually runs and tunes. I'd design the matching service to accept a short batching window as a tunable parameter rather than hardcoding either extreme.

Break the ETA into stages, restaurant prep time remaining, travel time from restaurant to Dasher's current position if not yet picked up, and travel time from pickup to the customer, then sum them. Each stage pulls from a different signal: prep time from the restaurant's historical average for that item mix and current order queue depth, travel time from a routing service that factors in live traffic, not just straight-line distance or a static speed assumption.

I'd treat this as a model that gets recomputed periodically as new signals arrive, current Dasher GPS position, updated prep time from the restaurant's kitchen display system, rather than a single estimate computed once at order placement and never touched again. The honest failure mode to bring up unprompted: a bad prep-time estimate compounds into every downstream ETA shown to the customer, so the system needs a fallback (a wider, more conservative estimate) when the restaurant's kitchen signal is missing or stale, rather than silently trusting a number that might be garbage.

The key constraint is that "search" here is really "search within a delivery radius of this exact address," which means every query is geo-filtered before it's ranked. I'd shard the restaurant index by region, geohash or a similar spatial partitioning scheme, so a query only has to search restaurants in the customer's local cell and a small ring of neighboring cells, instead of scanning a national index and filtering afterward.

Within that filtered set, ranking blends relevance to the search term with operational signals, restaurant rating, historical prep and delivery reliability in that zone, current Dasher availability nearby, since a restaurant with a great menu but no Dashers to fulfill it right now is a bad top result. I'd keep the geo-filter and the ranking model as separate stages rather than one combined scoring function, since that separation lets you tune ranking without re-architecting how regions are partitioned.

Separate the system into event producers (order service, promotions service, each emitting events to a shared queue or pub/sub topic) and a notification dispatcher that consumes those events and decides how to deliver them, push notification, SMS, email, based on the customer's stored preferences and the urgency of the event type. Order status changes need low-latency delivery; a promotional email can tolerate a delayed batch send.

The part worth calling out unprompted: idempotency and preference respect matter more here than raw throughput. A retry after a transient failure shouldn't double-send the same "your order is on the way" push, so every notification needs a dedup key, and a customer who's opted out of promotional pings needs that respected at the dispatcher level, not left to each individual producer to remember correctly.

This is one of the specific system design prompts candidates report getting, and the core challenge is that pricing has to update fast enough to reflect real conditions without flickering wildly between two customers checking out ninety seconds apart on the same order. I'd design a pricing service that ingests near-real-time signals, current open-order count per zone, available Dasher count per zone, distance and estimated time, and computes a fee through a model that's recomputed on a short interval (every 30 to 60 seconds) per zone rather than per individual request, then caches that zone-level multiplier for any request landing inside that window.

The failure modes worth surfacing without being asked: a pricing update mid-checkout shouldn't change the fee a customer already saw and is about to pay, so you lock in the fee at cart-open and honor it for a short grace window even if the zone multiplier shifts underneath. And the model needs guardrails, a max multiplier and a max fee, both because an unbounded surge fee is a trust problem with customers and because a pure "let the market clear" model has genuinely blown up in public before for other companies in this space.

How to prepare for a DoorDash interview

Practice the OA's actual shape before anything else, two dense, rule-heavy problems in 90 minutes on HackerRank, not a single elegant LeetCode Medium. Time yourself on problems that require tracking several simultaneous constraints, an interval schedule with a capacity limit, a simulation with tie-break rules, since the failure mode candidates report isn't algorithmic, it's misreading one rule buried in a long prompt. For the coding and code-craft rounds, practice explaining your naming and structure choices out loud as you write, not just after the interviewer asks, since DoorDash explicitly grades readability as part of the round, not as an afterthought.

For system design, anchor every answer in the actual product, dispatch, live tracking, ETA, search, rather than a generic template memorized from a different company's interview guide, and be ready to defend the specific trade-off DoorDash's marketplace creates: speed for one side of the marketplace can quietly cost the other side. If you're interviewing for strategy, ops, product, or data roles, practice framing every case answer around all three sides of the marketplace, consumers, merchants, and Dashers, since a case answer that only addresses one side reads as incomplete even when the math is right.

Get the reps in before the real thing

LastRoundAI's mock interview mode runs live coding and system design rounds with real follow-up questions in your browser, the same kind of pressure a DoorDash onsite loop applies when an interviewer changes a constraint mid-answer. The free plan includes 15 credits a month that reset monthly, and Starter is $29/mo (or $19/mo on the 3-month plan) if you want more sessions before a real loop.

Once your answers hold up under a follow-up, Auto-Apply queues tailored applications for roles like this for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

How this list was built

Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.

What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.

If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.

Frequently asked questions

How long is the DoorDash hiring process?

Often three to eight weeks end to end, with the gap between onsite and decision being the slowest part. Team matching, where it applies, can add further time and is not a reflection on your performance.

How many interview rounds does DoorDash have?

Usually a recruiter screen, a technical phone screen, then an onsite loop of four to five rounds covering coding, system design and behavioural. The exact count shifts by level and team, and some loops add a domain-specific round, so ask your recruiter for the actual schedule.

How hard is the DoorDash interview?

Hard, but the difficulty is more about depth of follow-up than exotic questions. Interviewers tend to take a reasonable problem and keep pushing on trade-offs, edge cases and what you would change at ten times the load. Preparing to be interrogated on an answer matters more than memorising more answers.

What coding level should I prepare for a DoorDash interview?

Comfortable medium-difficulty problems solved cleanly while talking, rather than hard problems solved silently. Most rejections come from unclear communication and untested edge cases, not from failing to find an exotic algorithm.

Does DoorDash ask system design at every level?

From mid-level upward, almost always, and it carries increasing weight as you go up. Junior loops may include a lighter version focused on structuring a feature rather than designing a distributed system.

Sources & further reading

Leave a Reply

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