DoorDash Full-Stack Developer Interview Preparation Guide - Mid Level
DoorDash's full-stack developer interview process evaluates technical proficiency across frontend and backend development, system design thinking, problem-solving within their platform context, and cultural fit. The process includes phone-based technical screening followed by onsite rounds covering coding, system design, behavioral assessment, and real-world problem-solving scenarios relevant to a food delivery platform.
Interview Rounds
Recruiter Screening
What to Expect
Initial conversation with a recruiter to assess background, motivation, and basic fit for the role. This combined round covers both initial screening and recruiter follow-up conversations. The recruiter will verify your full-stack development experience, discuss your interest in DoorDash, and confirm logistical details for upcoming interviews.
Tips & Advice
Prepare a clear 2-3 minute overview of your professional journey emphasizing full-stack projects. Highlight specific examples where you've built or maintained both frontend and backend components. Research DoorDash's mission and express genuine interest in their platform challenges. Ask thoughtful questions about the team structure and technical stack. Be honest about your experience level—recruiters appreciate authenticity about what you know and what you're eager to learn.
Focus Topics
Technical Stack Familiarity
Overview of frontend frameworks (React, Vue, Angular), backend languages (Python, Java, Node.js), databases, and deployment platforms you've worked with.
Practice Interview
Study Questions
Motivation for DoorDash
Clear articulation of why you're interested in DoorDash specifically, ideally connecting your skills to problems the platform solves or technologies they use.
Practice Interview
Study Questions
Full-Stack Development Background
Overview of your experience building complete web applications, including specific projects where you owned frontend UI development, backend API logic, and database design simultaneously.
Practice Interview
Study Questions
Technical Phone Screen - Coding
What to Expect
Live coding assessment conducted over video call with a software engineer. You'll receive a moderate-difficulty coding problem requiring algorithmic thinking and clean implementation. The focus is on your problem-solving approach, code quality, and ability to handle follow-up questions or variations on the problem.
Tips & Advice
Communicate your thought process throughout—explain your approach before coding. Start with a brute force solution, then optimize. Ask clarifying questions about edge cases. Write clean, readable code with appropriate variable names. Test your solution with provided examples and discuss time/space complexity. Be prepared for follow-ups like 'what if we add this constraint?' For full-stack context, problems may involve order routing, delivery calculations, or restaurant data structures similar to DoorDash operations.
Focus Topics
Code Quality and Communication
Writing clean, readable code with meaningful variable names, appropriate comments, and structured logic. Verbalizing your reasoning throughout the problem-solving process.
Practice Interview
Study Questions
Time and Space Complexity Analysis
Understanding and articulating big-O complexity of your solutions, recognizing trade-offs between runtime and memory usage, and optimizing when necessary.
Practice Interview
Study Questions
Algorithm Design and Problem-Solving
Proficiency in analyzing moderate-complexity problems, identifying appropriate data structures (arrays, hashmaps, heaps, trees), and implementing efficient solutions with clear logic.
Practice Interview
Study Questions
Onsite - Coding Round 1
What to Expect
First of multiple onsite technical interviews focused on coding ability. You'll solve 1-2 algorithmic problems of medium to medium-hard difficulty. The engineer will assess your coding fluency, problem decomposition, and debugging skills. Unlike the phone screen, you may code in an IDE on a shared screen and have slightly more time to think.
Tips & Advice
Treat this as a slightly elevated challenge from the phone screen. Feel free to ask clarifying questions and think out loud. If you get stuck, communicate that and propose alternative approaches. Demonstrate willingness to iterate. Problems may relate to DoorDash domain—e.g., computing delivery driver pay, batching deliveries by location/time, or filtering restaurants by criteria. Use this as an opportunity to ask follow-up questions that show you're thinking about real-world implications.
Focus Topics
Data Structure Selection and Optimization
Choosing optimal data structures (heaps for priority ordering, hashmaps for quick lookup, trees for hierarchical data) to solve problems efficiently and implementing standard algorithms correctly.
Practice Interview
Study Questions
Debugging and Edge Case Handling
Systematically identifying bugs in your code, handling boundary conditions (empty inputs, single elements, duplicates), and thoroughly testing before declaring completion.
Practice Interview
Study Questions
DoorDash Domain-Specific Algorithms
Algorithmic problems related to delivery platform operations: computing dasher/courier pay structures, optimizing delivery batches, matching deliveries to drivers, and handling peak-hour scenarios.
Practice Interview
Study Questions
Onsite - System Design Round
What to Expect
Comprehensive system design interview where you architect a full system relevant to DoorDash's platform. You might be asked to design a personalized restaurant recommendation engine, a resilient dasher payment system, a donation platform with aggregation, or an API gateway handling multiple downstream services. The interviewer evaluates your ability to think about scale, reliability, databases, caching, API design, and trade-offs between consistency and availability.
Tips & Advice
Start by clarifying requirements and constraints (scale, QPS, latency targets, geographic distribution). Discuss high-level architecture first before diving into details. For a full-stack perspective, explicitly address both frontend and backend considerations—e.g., how frontend requests flow through load balancers, CDN caching for consumer data, and backend service communication. Draw diagrams showing services, databases, message queues, and APIs. Discuss trade-offs (SQL vs NoSQL, eventual consistency, caching strategies). At mid-level, you're expected to know foundational concepts deeply; you don't need to design ultra-complex distributed systems, but you should reason clearly about why specific choices matter. Ask questions if requirements are ambiguous.
Focus Topics
API Design and Service Communication
Designing RESTful or gRPC APIs for service-to-service communication, error handling, rate limiting, monitoring, and handling failures gracefully. Understanding synchronous vs asynchronous patterns.
Practice Interview
Study Questions
Database Selection and Schema Design
Choosing between relational databases (PostgreSQL), NoSQL stores (MongoDB, DynamoDB), and specialized stores (Redis, Elasticsearch). Designing schemas that support access patterns efficiently.
Practice Interview
Study Questions
Full-Stack System Design Thinking
Designing systems that consider both frontend (user experience, data loading, state management) and backend (service architecture, databases, caching) implications. Understanding how frontend decisions like pagination or infinite scroll affect backend queries.
Practice Interview
Study Questions
Scalability and Trade-off Analysis
Analyzing system behavior under load, choosing between consistency and availability (CAP theorem), database partitioning strategies, caching layers, load balancing, and explaining why specific trade-offs make sense for given requirements.
Practice Interview
Study Questions
DoorDash Platform Architecture Patterns
Understanding DoorDash-specific design patterns: resilient payment systems for dasher payouts, multi-service API aggregation for consumer requests, real-time delivery tracking, and marketplace matching algorithms.
Practice Interview
Study Questions
Onsite - Behavioral and Collaboration Round
What to Expect
Discussion with a senior engineer or team lead focused on your communication style, collaboration ability, ownership mentality, and how you handle challenges. You'll discuss past projects, conflict resolution, learning from failures, and how you approach mentoring or being mentored. This round assesses cultural fit and team dynamics.
Tips & Advice
Use the STAR method (Situation, Task, Action, Result) for concrete examples. For mid-level candidates, prepare stories showing: ownership of end-to-end features, collaboration across frontend/backend (or with other teams), handling technical disagreements constructively, mentoring junior engineers, learning from failures, and adapting to ambiguity. Avoid generic answers—be specific about what YOU did. Show humility about what you don't know and eagerness to learn. Ask thoughtful questions about team dynamics, mentorship expectations, and how decisions are made at DoorDash.
Focus Topics
Handling Ambiguity and Setbacks
Stories about navigating unclear requirements, recovering from project failures or technical mishaps, and adapting when circumstances change. Showing resilience and problem-solving creativity.
Practice Interview
Study Questions
Cross-Functional Collaboration
Examples of working effectively with backend, frontend, or platform engineers. Showing how you aligned on APIs, handled technical disagreements, and communicated about trade-offs.
Practice Interview
Study Questions
Mentorship and Growth Mindset
Examples of helping junior engineers, learning from more senior colleagues, seeking feedback, and growing your skills. Showing curiosity about technologies outside your comfort zone.
Practice Interview
Study Questions
Full-Stack Project Ownership
Concrete examples of projects where you owned complete features from frontend UI through backend logic and database. Describing challenges you overcame, technical decisions you made, and how you communicated trade-offs to stakeholders.
Practice Interview
Study Questions
Onsite - Debugging and Real-World Problem-Solving Round
What to Expect
Practical round where you debug or enhance an existing codebase, or solve a real-world problem with incomplete specifications. You may receive starter code with intentional bugs in a service (e.g., a dasher selection component or round-robin load balancer), or be asked to implement a feature with requirements that evolve during the interview. This assesses your ability to reason about complex systems, troubleshoot methodically, and adapt full-stack thinking to practical scenarios.
Tips & Advice
Read the provided code carefully before making changes. Identify the root cause of bugs by reasoning about expected behavior vs actual behavior. Write clear test cases to verify fixes. If requirements are vague, ask clarifying questions and make reasonable assumptions. For full-stack context, consider both frontend interactions and backend implications. This is your chance to show pragmatism and realistic thinking—production systems are messy, and you need to navigate that effectively. Communicate your debugging process throughout.
Focus Topics
Real-World Constraints and Trade-offs
Understanding that production systems have constraints (backward compatibility, performance targets, deployment windows). Proposing solutions that work within existing architecture while being pragmatic about limitations.
Practice Interview
Study Questions
Code Quality and Testing
Writing clean fixes that don't introduce new bugs, adding tests to prevent regression, and considering edge cases in your solution. Demonstrating thoughtful code review mentality.
Practice Interview
Study Questions
DoorDash Service Debugging
Debugging realistic problems in DoorDash services: fixing load balancer issues with dasher selection, resolving race conditions in payment processing, or correcting data aggregation bugs in donation/metrics systems.
Practice Interview
Study Questions
Full-Stack Problem Diagnosis
Systematically isolating problems across layers—determining if issues originate in frontend requests, API contracts, backend services, database queries, or infrastructure. Using logs and reasoning to trace failures.
Practice Interview
Study Questions
Frequently Asked Full-Stack Developer Interview Questions
You made a small but individually critical contribution inside a much larger project. How do you present that as your achievement?
Sample Answer
Direct answer
When your real contribution was small in scope, sell it on ownership and consequence, not size. Pick a moment where you personally diagnosed the problem, decided the fix, and were accountable for it landing safely, then connect it explicitly to the larger outcome it protected. The story works when the interviewer can see you understood why the change mattered, not just that you wrote it.
How to select and structure the story
- Selection test: would the project have shipped, or shipped safely, without this piece? If the honest answer is yes, pick a different story, "critical" has to be load-bearing, not just present.
- Ownership language: use verbs that show you made the call ("I traced it to...", "I decided to fix it by...", "I validated with...") rather than passive description of what happened around you.
- Scope honesty: name your actual boundary up front ("my piece of a much larger release was...") so the interviewer calibrates correctly instead of feeling misled later.
- Criticality proof: point to a concrete consequence if it had gone unfixed (an outage class, a compliance gap, a data-correctness issue), not just "it was important."
Worked example (skeleton)
This one is a software-engineering scenario; swap in your own domain's equivalent (a usability retest for a design fix, a re-run against held-out data for a model fix, a pilot group for a process fix) while keeping the same shape: diagnosis, decision, verified proof.
Situation: a shared backend service had a caching bug that occasionally served stale data to a subset of requests, one root cause inside a release many engineers were shipping together.
Task: I owned finding the exact fault and landing a safe, verified fix.
Action: I wrote a small reproduction test with a mocked clock (a fake, controllable version of the system clock, used to force a rare timing bug to happen on demand) to trigger the race (a race condition, where the bug only shows up depending on the exact order two things run in) deterministically, confirmed it failed on the current code, made a 3-line fix, and got the test to pass. I opened a PR with the reproduction steps and the failing-then-passing test output, walked a reviewer through the concurrency reasoning, and shipped behind a canary (released to a small slice of traffic first, so a bad fix is caught before it reaches everyone).
Result: before the fix, our error-tracking dashboard showed the stale-read error on about 40 requests a day; a week after the fix it was at 0 on the same dashboard. Going from 40 to 0 is a 100% reduction on that specific error class, a number I can point to on the same export, not one I'm estimating from memory.
Trade-offs and pitfalls
- Overclaiming (describing the whole release as "my project" when you owned one fix) invites a scope challenge you can't win. Underclaiming a genuinely critical fix in vague team language wastes a good story just as badly.
- If you can only describe your three lines and not why the bug existed or who else it touched, the story reads as luck rather than skill.
- A fix without a reproduction test or before/after evidence is a claim, not a demonstrated result, which is exactly the credibility a small-contribution story depends on.
You're kicking off a project that depends on several other teams delivering their pieces on time. How do you surface those dependencies early instead of discovering them midway through?
Sample Answer
Direct answer
Before committing to a plan, spend the first days mapping every team your work actually depends on, get an explicit, dated commitment from each one on what they will deliver, and track those commitments in one visible place so a slip surfaces the moment it happens instead of at the deadline.
Structured elaboration
Map the dependency graph early, not incidentally
Run a short cross-functional session at kickoff specifically to list what you need from other teams: what, by when, and in what form. Treat this as a deliverable of the kickoff, not a side conversation that happens if someone remembers to ask.
Get commitments, not assumptions
"They know we need this" is not a commitment. A commitment has an owner, a date, and an explicit acceptance criterion, meaning what "done" looks like from your side, not just theirs. Ambiguous handoffs are where dependencies quietly slip.
Make status visible continuously, not just at standups
A shared dependency tracker, checked weekly at minimum, with a clear ready, at risk, or blocked status per item, turns a hidden slip into a visible one while there is still time to react.
If you are joining an initiative already in motion
The mapping happens differently. Your first days are spent finding out who currently owns each piece, which may not match the org chart or what the original plan assumed, and estimating the time-to-impact for each dependency, meaning how long before a slip there would actually hit your own critical path (the specific chain of dependent tasks whose delay would directly delay your own delivery date, unlike a dependency that has slack to spare), before you commit to a timeline of your own. Committing to a date before doing this is committing to someone else's assumptions.
Worked example
A project depends on three other teams: one providing a new data feed, one exposing an API endpoint, and one delivering a design system component. At kickoff, the team runs a short dependency-mapping session and gets each provider to commit to a specific date and a specific definition of ready, for the API that means a documented contract and a staging environment, not just "the code exists." These commitments go into a shared tracker with a status column, reviewed weekly.
In week two, the API team's status moves to at risk because their own upstream dependency slipped. Because the tracker surfaced this immediately rather than at the original deadline, there is still time to either help unblock the API team or replan the timeline around a slower path, instead of discovering the problem in the final week when no good options remain.
For the joining-in-progress case: an engineer joins a multi-team initiative already underway. In the first few days, instead of accepting the existing plan at face value, they interview each team named in the plan to confirm who currently owns each dependency, since ownership has quietly shifted since the plan was written, and estimate the time-to-impact of each one: the API dependency would only hurt the timeline if it slipped more than two weeks, while the data-feed dependency has almost no buffer at all. Only after that mapping do they commit to a delivery date of their own, rather than inheriting the original plan's assumptions unchecked.
Trade-offs and pitfalls
A heavy dependency-tracking process on a small, low-risk project wastes more time than it saves; scale the rigor to the size and risk of the dependency rather than applying it uniformly everywhere.
The most common failure is treating the mapping as a one-time kickoff exercise instead of a living tracker. A dependency list that is accurate on day one and never updated again is exactly as useless as never having made one, because the whole point is catching drift as it happens.
Design a cache that must support get(key) and put(key, value), both in O(1) time, with a fixed capacity: once full, the least-recently-used entry is evicted to make room for a new one. Walk through the data structures you would combine to hit that O(1) bound on both operations and why a single hash map alone cannot do it.
Sample Answer
Direct answer
A hash map alone gives O(1) key lookup but has no notion of access order, so evicting the least-recently-used (LRU) entry would mean scanning every entry to find it, which is O(n). Pairing the hash map with a doubly linked list solves this: the hash map maps each key directly to its node in the list, and the list keeps nodes ordered by recency, most-recently-used at the head and least-recently-used at the tail, so a lookup, a reorder-on-access, and an eviction (pop the tail) are all O(1).
Approach
- Maintain a doubly linked list of nodes
(key, value), ordered by recency, with dummy head and tail sentinels so inserting or removing at either end never has to special-case an empty list. - Maintain a hash map from key to the node holding that key, so you never search the list by value; you always jump straight to the node.
get(key): if the key isn't in the map, return the miss sentinel. Otherwise move that node to the front (most-recently-used position) and return its value.put(key, value): if the key exists, update its value and move it to the front. Otherwise create a new node, add it to the front, and if that pushes the map over capacity, remove the node just before the tail sentinel (the actual least-recently-used entry) and delete it from the map too.
class Node:
__slots__ = ("key", "value", "prev", "next")
def __init__(self, key=None, value=None):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.map: dict[int, Node] = {}
self.head = Node()
self.tail = Node()
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node: Node) -> None:
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_front(self, node: Node) -> None:
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key: int) -> int:
if key not in self.map:
return -1
node = self.map[key]
self._remove(node)
self._add_to_front(node)
return node.value
def put(self, key: int, value: int) -> None:
if self.capacity <= 0:
return
if key in self.map:
node = self.map[key]
node.value = value
self._remove(node)
self._add_to_front(node)
return
node = Node(key, value)
self.map[key] = node
self._add_to_front(node)
if len(self.map) > self.capacity:
lru = self.tail.prev
self._remove(lru)
del self.map[lru.key]
if __name__ == "__main__":
cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
print(cache.get(1)) # 1 (1 is now most recent)
cache.put(3, 3) # capacity 2: evicts key 2 (least recently used)
print(cache.get(2)) # -1
cache.put(4, 4) # evicts key 1
print(cache.get(1)) # -1
print(cache.get(3)) # 3
print(cache.get(4)) # 4
Running this prints, in order: 1, -1, -1, 3, 4, which matches the standard LRU trace (put 1, put 2, get 1 promotes key 1, put 3 evicts key 2 since it's now the least recent, put 4 evicts key 1 since 3 was inserted more recently).
Key points
- Every present key has exactly one node in the map and exactly one node in the list; those two structures are kept in lockstep on every operation.
- The head and tail sentinels remove every edge-case branch for "the list is empty" or "the list has one element" from
_removeand_add_to_front. - Deterministic cache keys: the map key must be a stable, complete representation of the thing being cached. If two logically identical requests can hash to different keys, for example because a key is built from an unordered dict or kwargs whose iteration order isn't fixed, or because it omits a parameter that actually affects the result, you get both a spurious cache miss and duplicate storage for what should have been one entry. Build cache keys from a canonical, fully-ordered encoding of every input that affects the output.
- LRU vs. LFU: this design evicts by recency. A least-frequently-used (LFU) policy instead evicts the entry with the smallest access count, which needs a frequency counter per entry plus a way to find the minimum count in O(1) (typically a doubly linked list of frequency buckets, each bucket holding the keys at that frequency). LFU is worth reaching for when recency is a noisy signal, for example a periodic bulk scan that touches every key once would evict your actual hot set under plain LRU even though those keys are still frequently used elsewhere.
Complexity
Time: O(1) for get and put. Space: O(n) for n cached entries (one hash map entry plus one list node per key).
Edge cases
capacity <= 0:putis a no-op andgetalways misses.- Updating an existing key on
putmust still move it to the front; forgetting that is the most common bug in this design. - Capacity of exactly 1: every new
putafter the first immediately evicts the previous entry. - A workload dominated by a one-time bulk scan defeats plain LRU by evicting the genuinely hot set; that's the scenario where LFU or a hybrid recency-plus-frequency policy earns its extra bookkeeping.
Explain strong consistency versus eventual consistency and give practical examples of where a full-stack developer would require strong consistency and where eventual consistency is acceptable in a web application UX. Mention read-after-write expectations and user-facing consequences.
Sample Answer
Definition — strong vs eventual consistency
Strong consistency (linearizability) guarantees that once a write completes, all subsequent reads return that write. Eventual consistency guarantees that if no new writes occur, replicas will converge to the same value over time; reads can return stale data briefly.
When full-stack needs strong consistency
- Examples: banking balance updates, inventory checkout finalization, authentication/authorization changes.
- Why: user expects read-after-write correctness (e.g., after a withdrawal, balance must reflect new value). Use transactions, single-writer primary, or consensus (Paxos/Raft) to ensure this.
When eventual consistency is acceptable
- Examples: social feeds, analytics counters, comment like counts, search indexing.
- Why: small staleness is tolerable for better latency and availability. Systems use asynchronous replication, caches, or CRDTs.
Read-after-write expectations & UX consequences
- Strong: immediate confirmation; UI can show final state and remove rollback paths.
- Eventual: show optimistic UI with pending indicators, disable actions that require strong guarantees, or surface “last updated” timestamps. If unstated, stale reads can confuse users (e.g., posting a comment that doesn't appear).
Practical guidance
- Default to strong for money, security, inventory reservation at commit time.
- Use eventual for high-scale, high-latency features; compensate with clear UX (spinners, pending state, reconciliation).
- Consider hybrid: strong for critical fields, eventual for derived/denormalized views.
REST requires the server to hold no client session state between requests. Explain what statelessness does and does not forbid (a server may still hold data about the resource itself, just not about a specific client's conversation), and describe two concrete techniques for handling per-user needs like login sessions without server-side session state. What does statelessness buy you operationally when traffic spikes and an instance needs to be replaced, and what do you give up?
Sample Answer
Direct answer. Statelessness means every request must carry everything the server needs to process it: authentication, the resource being addressed, any filters or pagination position. The server is not allowed to remember what this client was doing between one request and the next. It is allowed to hold state about a resource (a row in a database), just not state about a specific client's conversation.
What it forbids, concretely. The classic violation is a login session: request 1 authenticates and the server stores "this session id is now logged in as user 42" in server memory; request 2 arrives with only the session id and the server looks up who that is from its own memory. That is exactly the per-client conversational state statelessness prohibits, because it means request 2 can only be served correctly by the specific server instance that handled request 1.
Two techniques that avoid it.
- Signed, self-contained tokens (e.g. a JSON Web Token, JWT). The client presents a token on every request; the server verifies its signature and reads the user identity and permissions directly out of the token, with no server-side lookup of who this session is. Any server instance can validate any request with only its own signing key, which is what makes statelessness pay off: you can add or remove instances freely.
- A session id backed by a shared, external store (for example Redis). The server still looks up session data, but the data lives outside any one instance's memory, so any instance can serve any request by querying the shared store. This is a middle ground: it is stateless from the server instance's point of view, even though state still exists somewhere.
What you get, and what you give up, when traffic spikes. With true statelessness (technique 1), you can add ten more instances behind a load balancer during a spike and route any incoming request to any of them, with zero coordination needed between instances, and you can kill an unhealthy instance immediately without worrying about losing anyone's conversation. What you give up: revocation is harder (a signed token is valid until it expires; you cannot instantly invalidate one without an extra deny-list mechanism), and the token itself grows with however much identity or permission data it carries, adding a small amount of bytes to every single request.
Trade-offs and pitfalls. Teams often reach for the shared-store approach (technique 2) because it feels like a smaller change from an in-memory session, but it quietly reintroduces a single dependency every request now needs, and if that store is slow or down, every request is affected, which is a different failure mode than the server that happened to hold your session being down.
What is a closure, and what does it capture from its enclosing scope? Explain, with a small code example, how a closure or a callback holding a reference can keep an object alive longer than expected (for example through a reference cycle), and describe a practical strategy to avoid or detect that kind of memory retention in a long-running process.
Sample Answer
Direct answer
A closure is a function bundled together with references to the variables from its enclosing scope that it uses, captured by reference (not by value), so it keeps seeing the CURRENT value of those variables even after the enclosing function has returned. That captured reference can create a reference cycle, which is why a closure or callback can keep an object alive longer than you expect.
Structured elaboration
- What gets captured: a closure captures the variable itself (technically, the enclosing scope's cell), not a snapshot of its value at creation time. Two closures created from the same enclosing call share independent state; two closures created from the SAME variable in a loop share the same captured cell, which is the classic 'all my callbacks report the same, final loop value' bug.
- Why closures can leak memory: a closure keeps a live reference to everything it captures for as long as the closure itself is reachable. If you then store that closure back onto an object it captured (a callback registered on the very object it was built from), you've created object -> closure -> object, a reference cycle.
- Why reference counting alone can't free a cycle: CPython's primary memory management is reference counting, an object is freed the instant its reference count hits zero. In a cycle, each object holds a reference to the other, so neither one's count ever reaches zero on its own, even after nothing OUTSIDE the cycle references either of them. This is precisely why CPython also runs a separate cyclic garbage collector (
gcmodule) that periodically looks for groups of objects that reference each other but are unreachable from anywhere else, and frees them as a group. - Mitigation strategies: avoid storing a closure back onto the object it captures when you can restructure to avoid it; use
weakreffor a back-reference that shouldn't keep the target alive (a common pattern for observer/callback registries); or simply trust the cyclic collector for genuinely short-lived cycles and only investigate further if profiling shows real, growing retention in a long-running process.
Worked example
class Node:
def __init__(self, name):
self.name = name
self.on_event = None
def wire(node):
def handler(): # closure: captures `node`
return f"{node.name} handled"
node.on_event = handler # node -> handler -> node : a cycle
return handler
Verified by running it with gc.disable() and a weakref to the node: after del n (dropping the only external reference), the node is STILL alive (ref() is not None is True) because the cycle keeps both objects' reference counts above zero. Re-enabling the collector and calling gc.collect() reclaims it (ref() is None becomes True immediately after), confirming the cyclic collector, not reference counting, is what actually frees this pattern.
Trade-offs & pitfalls
In a long-running service, this usually shows up as slow, steady memory growth rather than an obvious crash, because the cyclic collector DOES eventually run and free most cycles; the real danger is cycles involving objects with a __del__ method (historically these were UNCOLLECTABLE by the cyclic GC before Python 3.4, and even post-3.4 they add real collection overhead) or large cycles that make each collection pass more expensive as the live object graph grows. The fix is rarely 'stop using closures', it's to be deliberate about back-references specifically, using weakref where a callback registry would otherwise hold the only thing keeping a large object graph alive.
Explain the difference between correlation and causation in the context of incident diagnostics. Provide a concrete example where a metric (e.g., increased error rate) correlates with a recent deployment but is actually caused by a third variable (for example, a traffic reroute). Describe at least three methods you would use to test true causation versus coincidental correlation.
Sample Answer
Correlation is "these two things moved together"; causation is "one of them made the other happen." An incident-diagnostics investigation that stops at correlation risks fixing (or blaming) the wrong thing.
Worked example
Error rate rises right after a deploy. That's a correlation. The actual cause might be the deploy itself, or it might be a third variable: a traffic reroute that happened to land at the same time, sending a different, more error-prone traffic mix to this service, with the deploy being coincidental.
Three ways to test true causation
- Precise timing alignment: does the error rate change at the exact deploy timestamp (to the second/minute), or does it lag or lead it? A reroute event with its own distinct timestamp that better matches the error onset is evidence against the deploy being the cause.
- Controlled comparison: roll the deploy back on a subset (a canary) while leaving the traffic conditions unchanged, and see if errors clear on just that subset; if errors persist despite the rollback, the deploy wasn't the cause.
- Mechanism check: can you point to a specific code/config change in the deploy that plausibly produces this exact error, versus the reroute plausibly sending edge-case traffic (different geography, different client version) that the service already mishandled before the deploy.
Generalizing to model diagnostics
The identical discipline applies to a model metric correlating with a deployment event: before concluding the new model caused a metric drop, check whether a confound (a simultaneous data-pipeline change, a seasonal effect, a shift in the underlying user population) explains it just as well, and use the same three tests (timing precision, a controlled comparison holding the model fixed while varying the other candidate, and a plausible mechanism) before attributing cause.
Trade-offs and pitfalls
Under time pressure, mitigating based on correlation alone (rolling back the most recent deploy because it's the most recent) is often the right immediate action even before causation is proven, but the RCA writeup should not present that correlation as a proven root cause without the additional checks above.
Your team reduced the authentication endpoint's p95 latency from 500ms to 350ms, a 30% improvement. For three audiences: a non-technical CEO, external developer customers, and internal engineering managers, write a short tailored message explaining the business value and one key metric each audience should track.
Sample Answer
Direct answer
The number doesn't change across audiences, but what it's evidence for does: to a CEO it's a business outcome (conversion, retention, cost), to external developers it's a reliability guarantee they can build on, to internal engineering managers it's a load and capacity signal. Same 30% improvement, three different "so what," each with the one metric that audience should actually track next.
Structured elaboration
The technique is picking, per audience, which consequence of the number they actually own:
- Translate the metric into the currency that audience is measured on before stating it. A CEO is measured on revenue and retention; an external developer is measured on their own app's reliability; an internal engineering manager is measured on system load and incident risk.
- Give exactly one metric to track next, not a dashboard's worth. Too many numbers reads as "we're not sure which one matters"; one number reads as a clear owner and a clear signal.
- State the baseline and direction explicitly, down from 500ms to 350ms, not just "faster," so nobody has to ask what the improvement actually was.
This same three-move pattern is what you would reuse for a different metric to the same or different audiences: messaging a lazy-loading improvement to executives, sales, and developers, an API deprecation to engineering, customers, and executives, or a throughput doubling to a CTO, operations, and sales. The technique is constant; only which consequence you lead with changes per audience.
Worked example
p95 latency means the response time that 95% of requests are faster than, so it's a measure of how slow the worst-but-common cases are, not the average case.
To the CEO: "We cut the time it takes customers to sign in from half a second to a third of a second, 500ms to 350ms, a 30% improvement, on the slowest 5% of requests, the ones customers actually notice as lag. Faster sign-in means fewer people abandoning at login and less friction on every visit. Metric to track: conversion rate on the sign-up-to-first-action flow over the next two weeks, to see if that translates into fewer drop-offs."
To external developer customers: "Our authentication endpoint's p95 latency, the response time 95% of your calls beat, dropped from 500ms to 350ms. You should see fewer client-side timeouts and retries against this endpoint. Metric to track: your own timeout and retry rate against our auth endpoint, it should trend down."
To internal engineering managers: "We cut auth p95 from 500ms to 350ms through caching and query tuning, which lowers tail latency for every downstream service that calls auth before doing its own work. Metric to track: queue length and tail latency on the services immediately downstream of auth, to confirm the improvement is propagating rather than just moving the bottleneck."
Trade-offs and pitfalls
Reusing the same "30% faster" framing for every audience without a metric attached invites the follow-up "compared to what, and how would I know it's working," which is exactly what the per-audience metric answers in advance. It's also a mistake to promise a business outcome, like "this will increase conversion," as a fact rather than a hypothesis. Latency and conversion correlate but aren't guaranteed to move together for every product, so the honest version says "we would expect to see," not "this will." And if a later measurement shows a segment of customers saw no improvement, say a region or client version, that needs its own honest message rather than folding it quietly into the aggregate number.
Design a 30-60-90 day onboarding plan for a new hire joining your team. What do you prioritize in each phase, and how do you know they're on track?
Sample Answer
Direct answer
A good 30-60-90 plan moves someone from learning the environment, to contributing under supervision, to owning outcomes independently, with the phase boundaries defined by demonstrated behavior (what they can do unsupervised) rather than by the calendar alone. Track it with a small number of concrete, visible outputs per phase so "on track" is something you can point to, not just a feeling.
The three phases, by what changes
- Days 1-30 (learn and observe): environment setup, codebase or domain orientation, shadowing, and one small real contribution rather than a toy task, so the first change is real but low-risk.
- Days 31-60 (contribute under guidance): own a medium-sized piece of work end to end with a mentor available for review and unblocking, not doing it alongside them line by line.
- Days 61-90 (own outcomes): lead something (a project, an on-call rotation, a smaller onboarding task for the next hire) with the mentor as a backstop, not a co-pilot.
How you know they're on track
- Define the signal per phase in advance, not retroactively: for phase 1, did they reproduce the environment and ship one small real change without major help; for phase 2, is their review feedback shrinking in volume and severity over successive changes; for phase 3, can they make a reasonable decision alone and only escalate the genuinely hard calls.
- Check in on cadence (weekly early on, less frequent later) rather than waiting for day 30, 60, or 90 to find out something drifted three weeks ago.
Adjusting the plan for real constraints
- Limited training resources: when there's no dedicated ramp-up bandwidth (no spare mentor hours, no formal training material), lean harder on asynchronous artifacts: written runbooks, recorded walkthroughs, a curated list of the most representative recent changes, and a lighter-touch weekly sync instead of daily pairing. The phases stay the same; what changes is how much is self-serve versus live.
- Cross-skill ramp: if someone hired primarily for one skill set is expected to also ship in an adjacent one by day 90 (for example, a backend-focused hire expected to ship frontend work), that adjacent skill needs its own explicit milestone inside the plan, not an assumption it'll happen by osmosis. Concretely: days 1-30 stays focused on their strong area to build early confidence and trust; days 31-60 introduces the adjacent skill on a small, well-scoped, low-risk piece with close review; days 61-90 has them own something end to end in the new area, even if smaller in scope than their core-skill ownership.
Worked example
For a new hire joining an established codebase with a small team and no dedicated onboarding budget (the limited-resources case), the 30-60-90 looked like: days 1-30, self-serve environment setup using a written runbook plus a single half-day pairing session, culminating in one small, real bug fix; days 31-60, ownership of one medium feature with async review as the main touchpoint, and a short weekly 15-minute sync instead of daily check-ins; days 61-90, the new hire wrote the onboarding runbook update for the next person, which served double duty as both a real deliverable and a check on whether they actually understood the system well enough to explain it. Being on track was tracked by a short checklist per phase (environment reproducible, first fix merged with normal review effort, feature shipped with review comments trending down) rather than a single blanket "how's it going" check-in.
Trade-offs and pitfalls
- Treating the day boundaries as fixed calendar dates rather than behavioral milestones creates false confidence; someone can hit day 60 without actually being ready for phase-3 ownership, and pushing them into it anyway sets them up to fail.
- Under-supporting the adjacent-skill ramp (assuming a backend engineer will "pick up" frontend without an explicit milestone) is a common way cross-skill onboarding quietly fails; it needs the same structure as the primary skill, just smaller in scope.
- Compressing the plan under limited training resources by cutting phase 1 short (rushing into real ownership before the environment and codebase are understood) trades a faster-looking ramp for more review overhead and rework later.
Product tells you the system must 'handle spikes.' What clarifying questions and metrics would you ask for to turn that into a measurable constraint you can actually design against?
Sample Answer
Direct answer
Turn "handle spikes" into numbers by asking for the spike multiplier over baseline, its duration and arrival shape, the peak concurrency it implies, and what is allowed to degrade versus what must stay within the service-level agreement (SLA) during it. Those four answers are what actually let you size autoscaling, connection pools, and a degradation plan; without them, "handle spikes" is a feeling, not a requirement.
Structured elaboration
The four questions that make it measurable
| Ask | Why it matters | What it changes in the design |
|---|---|---|
| Spike multiplier (for example 5x, 10x baseline) | Sets the capacity ceiling | Autoscaling target and reserved headroom |
| Duration (seconds, minutes, hours) | Short spikes need fast reaction or buffering; long ones need sustained capacity | Whether you lean on autoscaling reaction time or pre-provisioned warm pools |
| Arrival shape (sudden burst, ramp, or periodic) | Changes what absorbs the shock | Rate limiting and queueing versus scheduled pre-scaling |
| What must stay within SLA versus what can degrade | Defines the failure mode you design for | A graceful-degradation plan (partial feature disabling, cached fallback, explicit error responses) instead of an undifferentiated outage |
The general skill, applied to a different vague ask
The same discipline works on any vague requirement, not just traffic spikes. "Handle a fifteen-year-old legacy system with no APIs" is exactly as unmeasurable until you ask the analogous questions: what data-access surfaces actually exist (direct database reads, nightly file exports, screen automation), who owns changes to that system, what staleness is tolerable in whatever gets extracted, and what happens to your system if that legacy system goes down for a day. "No APIs" becomes a concrete integration contract the same way "handle spikes" becomes a concrete capacity contract, by naming the constraint that changes the design instead of accepting the vague label.
Worked example: turning "5x for ten minutes" into a server count
Assume measured baseline steady-state traffic of 1,000 requests per second (RPS), and product says the spike is "5x for about ten minutes." Assume each server instance safely handles 200 RPS at target latency:
baseline servers=2001,000=5 spike RPS=5×1,000=5,000 spike servers needed=2005,000=25Now check whether autoscaling can even react in time. Assume it takes 3 minutes from scale-out trigger to a new instance serving traffic:
spike duration (10 min)>scale-out reaction time (3 min)Autoscaling alone is workable here, with roughly 3 minutes of degraded capacity at the start of the spike. If the same 5x spike instead lasted 60 seconds (a flash-crowd shape rather than a sustained one), the 3-minute scale-out reaction time would exceed the entire spike duration, and the only real fix is pre-warmed standby capacity, not faster autoscaling. That is why duration and arrival shape change the design, not just the multiplier.
Trade-offs & pitfalls
- Pitfall: designing for "handle any spike" instead of a bounded one. Every system has a ceiling; the point of these questions is choosing it deliberately instead of discovering it during an incident.
- Pitfall: assuming autoscaling reaction time is negligible. If it is not faster than the spike itself, pre-provisioned headroom is needed, which costs money sitting idle.
- Graceful degradation (returning cached or partial results, shedding low-priority requests) is usually cheaper than provisioning for the absolute peak, but only if product has said which features are allowed to degrade.
Want to create your own tailored preparation guide using our deep research?
Get Started for FreeInterview-Ready Courses
Visual-first, interactive, structured learning paths
Browse Full-Stack Developer jobs
AI-enriched listings across hundreds of company career pages
Explore Jobs