DoorDash Senior Full-Stack Developer Interview Preparation Guide
DoorDash's Full-Stack Developer interview process for Senior level candidates spans approximately 4-6 weeks and includes an initial recruiter screening, 2 technical phone screens, and 5 comprehensive onsite rounds covering coding algorithms, system design, behavioral assessment, incident investigation, and architectural deep-dives. The process evaluates both full-stack technical capabilities and the ability to drive large projects independently while mentoring others.
Interview Rounds
Recruiter Screening
What to Expect
Initial phone call with a recruiting coordinator or technical recruiter (15-30 minutes). They'll verify your background, discuss your interest in the role, confirm salary expectations, and assess cultural fit. For Senior-level candidates, expect detailed conversation about your experience leading projects and mentoring others.
Tips & Advice
Be enthusiastic about DoorDash's mission in the delivery space. Prepare 2-3 specific examples of senior-level impact: leading a major feature, mentoring team members, or owning an architectural decision. Be concise and authentic. This round is mainly about fit and logistics, not eliminating candidates. Mention specific technologies or DoorDash's product if possible to show genuine interest.
Focus Topics
Motivation for DoorDash and Full-Stack Role
Why full-stack development appeals to you, and specific aspects of DoorDash's business (delivery logistics, scale challenges, tech stack) that interest you
Practice Interview
Study Questions
Mentorship and Team Contributions
Specific instances where you've mentored junior engineers, conducted code reviews, or contributed to team growth
Practice Interview
Study Questions
Background and Career Narrative
Clear articulation of your 5-12 years of experience, progression to senior level, and relevance to full-stack development at a high-scale company
Practice Interview
Study Questions
Project Ownership and Leadership Examples
Concrete examples of projects you've led end-to-end, including scope, technical decisions, and team impact
Practice Interview
Study Questions
Technical Phone Screen - Coding Round 1
What to Expect
45-60 minute technical phone interview with a senior engineer via a shared code editor (typically CoderPad or similar). You'll solve 1-2 medium-to-hard coding problems from the areas of data structures, algorithms, or practical backend/frontend optimization. Problems often relate to DoorDash's domain (e.g., courier route optimization, inventory management, or payment calculations).
Tips & Advice
Start by clarifying the problem and asking clarifying questions before coding. For senior-level candidates, interviewers expect optimized solutions with clear complexity analysis. Think aloud about trade-offs and edge cases. If you get stuck, pivot: discuss your approach, mention brute-force first, then optimize. Senior candidates should also consider scalability and real-world constraints (e.g., 'what if we have millions of couriers?'). Code cleanly and be prepared to refactor or extend your solution.
Focus Topics
Algorithm Patterns: Dynamic Programming
Recognizing DP problems, building recurrence relations, memoization vs. tabulation, optimization of state space
Practice Interview
Study Questions
Algorithm Patterns: Search and Sort
Binary search, merge sort, quicksort, topological sort; variations like rotated search, k-largest element problems
Practice Interview
Study Questions
Algorithm Patterns: Graphs and Traversals
BFS, DFS, Dijkstra's, Bellman-Ford, connectivity, topological sorting; understanding directed vs. undirected graphs
Practice Interview
Study Questions
Practical Code Quality and Communication
Writing readable code with meaningful variable names, handling edge cases explicitly, explaining logic clearly, adjusting based on interviewer feedback
Practice Interview
Study Questions
Data Structures Mastery
Deep knowledge of arrays, linked lists, trees, heaps, hash tables, graphs; knowing when and why to use each; implementing custom structures when needed
Practice Interview
Study Questions
Coding Optimization and Complexity Analysis
Writing efficient code with strong understanding of time/space trade-offs; ability to optimize brute-force solutions; clear Big-O analysis
Practice Interview
Study Questions
Technical Phone Screen - Coding Round 2
What to Expect
45-60 minute technical interview similar in format to Round 2, but with different problem(s). May focus on backend-specific logic (API design, database queries, caching strategies) or frontend-specific patterns (state management, component design, performance optimization). Problems are typically medium-to-hard difficulty and may involve practical scenarios relevant to DoorDash's platform.
Tips & Advice
Approach this round as a different problem type to showcase breadth. If Round 2 was algorithmic, this might be more systems-oriented (e.g., 'design a caching layer' or 'implement a rate limiter'). Don't hesitate to ask about constraints: scale, latency, availability. For Senior Full-Stack roles, you might face a hybrid problem that touches both frontend and backend. Discuss trade-offs candidly and show you understand the full picture.
Focus Topics
Caching and Performance Optimization
Cache invalidation strategies, multi-level caching (browser, server, database), trade-offs between latency and consistency
Practice Interview
Study Questions
Database Query Optimization and SQL
Writing efficient SQL, understanding indexes, query plans, avoiding N+1 problems, denormalization trade-offs
Practice Interview
Study Questions
Frontend Problem Solving: State and UI Logic
Implementing interactive features, state management patterns, performance optimization, handling real-time updates
Practice Interview
Study Questions
Practical Coding Under Pressure
Managing time, making reasonable assumptions, asking for clarification, recovering from small mistakes, communicating clearly
Practice Interview
Study Questions
Backend Problem Solving: APIs and Business Logic
Designing REST/GraphQL endpoints, handling edge cases, validating input, computing complex queries (e.g., compute courier pay, batch allocation)
Practice Interview
Study Questions
Onsite - Coding and Algorithms Interview
What to Expect
90 minutes with a senior engineer in a whiteboard/IDE environment. You'll solve 1-2 coding problems of medium-to-hard difficulty, likely with a DoorDash domain flavor (e.g., 'compute dasher pay with peak-hour earnings', 'implement round-robin load balancer', 'minimize batches for delivery time points'). This round is deeper than phone screens; expect follow-up questions, extensions, and discussion of real-world scalability.
Tips & Advice
Use the full 90 minutes thoughtfully. Start with problem clarification and examples. For a senior candidate, interviewers expect you to naturally discuss edge cases, complexity, and scalability without prompting. If you finish the main problem early, ask for extensions: 'How would this change if we had 1M couriers?' or 'Can we optimize space further?' Be prepared to defend your choices and adapt your approach based on feedback. Whiteboard coding is different from IDE coding—be clear, write legibly, and explain as you go.
Focus Topics
Code Quality and Edge Case Handling
Handling off-by-one errors, null/empty cases, large numbers, negative inputs; clean, defensive code
Practice Interview
Study Questions
Problem Extension and Follow-Up Handling
Gracefully extending solutions, handling 'what if' scenarios, optimizing based on new constraints, showing flexibility
Practice Interview
Study Questions
Scalability and Real-World Constraints
Thinking about scale from the start: millions of users, high concurrency, latency requirements, distributed considerations
Practice Interview
Study Questions
Advanced Algorithm Patterns
Greedy algorithms, advanced DP, graph algorithms with real-world applications, bit manipulation, math-heavy problems
Practice Interview
Study Questions
Whiteboard and Communication Skills
Clear explanation of approach, step-by-step problem breakdown, effective use of whitespace, adapting to feedback
Practice Interview
Study Questions
DoorDash Domain Problems: Delivery and Payment Systems
Problems specific to courier operations: computing pay, handling peak-hour bonuses, batch allocation, route optimization
Practice Interview
Study Questions
Onsite - System Design Interview
What to Expect
60-90 minutes with a senior/staff engineer focusing on designing a large-scale system. Typical DoorDash system design topics include: designing a donation service platform, building a personalized restaurant recommendation system, designing a dasher payment system, or real-time delivery tracking. You'll discuss architecture, technology choices, scaling strategies, data models, API design, and trade-offs. Interviewers assess your ability to make sound architectural decisions for complex distributed systems.
Tips & Advice
Start by clarifying requirements and constraints (QPS, latency, consistency model, data volume). Draw a high-level architecture and iterate based on feedback. For senior-level candidates, go beyond basic HTTP+database; discuss caching layers, message queues, databases choices (SQL vs. NoSQL), and trade-offs explicitly. Address scalability: how does your design handle 10x or 100x growth? Be prepared to deep-dive into specific components (e.g., database schema, API endpoints, load balancing strategy). Senior engineers should also consider operational aspects: monitoring, logging, disaster recovery, and team ownership.
Focus Topics
Caching Strategy and Layers
Multi-level caching (browser, CDN, application, database), cache invalidation, hot vs. cold data, cache-aside vs. write-through patterns
Practice Interview
Study Questions
Load Balancing and Failover
Round-robin, consistent hashing, failover strategies, health checks, handling server crashes gracefully
Practice Interview
Study Questions
API Design and Backend for Frontend (BFF)
Designing scalable APIs (REST, GraphQL), versioning, handling concurrent requests, rate limiting, designing APIs for different client needs
Practice Interview
Study Questions
Scalability and Performance Optimization
Identifying bottlenecks, optimizing for latency and throughput, capacity planning, handling peak loads (e.g., surge pricing)
Practice Interview
Study Questions
Message Queues and Asynchronous Processing
Event-driven architecture, message brokers (Kafka, RabbitMQ), pub-sub patterns, handling failures and retries
Practice Interview
Study Questions
Distributed System Fundamentals
CAP theorem, eventual consistency, distributed consensus, replication strategies, partition tolerance; when to use strong vs. eventual consistency
Practice Interview
Study Questions
Database Selection and Schema Design
Choosing between SQL and NoSQL, schema design, indexing strategies, sharding/partitioning, handling transactions and consistency
Practice Interview
Study Questions
DoorDash Architecture Principles: Delivery Platform Design
Understanding large-scale delivery platform components: user service, restaurant service, order service, payment service, courier/dasher management, real-time tracking; how they interact
Practice Interview
Study Questions
Onsite - Incident Investigation and Debugging
What to Expect
60 minutes with a senior/staff engineer examining a real or realistic production issue. You may be given a buggy codebase (e.g., 'dasher selection component' or a 'round-robin load balancer with DashMap') and asked to identify and fix bugs. Alternatively, you might be presented with a scenario: 'couriers are receiving incorrect pay calculation' and asked to investigate root causes. This round tests your ability to debug complex systems, read unfamiliar code, and think like a production engineer.
Tips & Advice
Take time to understand the codebase structure before jumping to fixes. Ask questions: What is the failure mode? When did it start? What changed recently? Think systematically through layers: frontend, API, backend logic, database. For senior candidates, don't just fix the bug—think about how to prevent it (testing, monitoring, code review). Communicate your debugging process clearly. If you spot multiple bugs, prioritize by impact. Show your thought process: hypothesis, how to test it, what evidence you'd look for.
Focus Topics
Concurrency and Thread Safety Issues
Race conditions, deadlocks, memory visibility, mutex usage, atomic operations; debugging concurrent code
Practice Interview
Study Questions
Prevention and Long-Term Solutions
Beyond fixing the immediate bug: suggesting tests, monitoring, architecture changes, or code organization improvements to prevent recurrence
Practice Interview
Study Questions
Performance and Resource Leak Detection
Identifying memory leaks, CPU hotspots, unoptimized queries, connection pool exhaustion, cache misses
Practice Interview
Study Questions
Reading and Understanding Unfamiliar Code
Quickly grasping code intent, tracing execution flow, identifying data structures and patterns, spotting anomalies
Practice Interview
Study Questions
Systematic Debugging Methodology
Forming hypotheses, testing incrementally, using logging and debugging tools, narrowing down root causes
Practice Interview
Study Questions
Onsite - Behavioral and Leadership Interview
What to Expect
45-60 minutes with a senior manager or peer engineer focused on behavioral assessment, teamwork, leadership style, and alignment with DoorDash values. Expect questions about past projects, team conflicts, mentoring experiences, how you've handled failure, and your approach to learning. For senior-level candidates, this round also assesses your ability to influence decisions, drive initiatives, and contribute to team culture.
Tips & Advice
Prepare 5-7 concrete stories using the STAR method (Situation, Task, Action, Result) covering: leading a significant project, overcoming a technical challenge, mentoring someone, handling a conflict, learning from failure, and demonstrating initiative. For senior candidates, stories should showcase impact beyond yourself: how you influenced the team, improved processes, or mentored others. Be authentic and specific—avoid generic answers. Listen carefully to questions and tailor your story if needed. Ask thoughtful questions about the team and role to show genuine interest.
Focus Topics
Collaboration and Communication
Working across teams (frontend, backend, product, data), communicating technical concepts to non-technical stakeholders, handling disagreement
Practice Interview
Study Questions
Learning from Failure and Resilience
Specific example of a significant failure, what you learned, how you bounced back, changes you made as a result
Practice Interview
Study Questions
Handling Ambiguity and Making Decisions
Scenarios with incomplete information, balancing speed vs. quality, making calls with imperfect data, pivoting based on feedback
Practice Interview
Study Questions
Technical Leadership and Influence
Influencing architectural decisions, proposing technical initiatives, driving adoption of best practices, leading technical discussions
Practice Interview
Study Questions
Mentorship and Team Development
Specific examples of mentoring junior engineers, unblocking team members, conducting effective code reviews, fostering growth
Practice Interview
Study Questions
Project Ownership and End-to-End Delivery
Leading large features from conception through production, making architectural decisions, managing trade-offs, ensuring quality
Practice Interview
Study Questions
Onsite - Architecture and Project Deep-Dive
What to Expect
60-75 minutes with a senior or staff engineer diving deep into your past work. You'll discuss a significant project you've led or contributed to: the problem statement, your architecture decisions, trade-offs you made, challenges you overcame, and what you'd do differently. This round tests your ability to reason about complex systems, justify technical choices, and think at a high level about system design. For full-stack roles, expect questions about how frontend and backend were integrated, how you optimized for user experience and performance.
Tips & Advice
Choose a project where you played a significant role and can speak with authority. Prepare a 2-3 minute overview of the project (problem, your role, outcome). Be ready to go deep: why that architecture? What were alternatives? How would you scale it? What would you change now with new knowledge? Interviewers will dig into decisions—have clear reasoning. For full-stack context, explain how frontend and backend decisions were intertwined. Mention metrics or business impact if available (improved latency, user adoption, cost savings). Be honest about limitations and learnings.
Focus Topics
Scalability and Performance Optimization in Practice
How you optimized the project for scale: caching, database indexing, API optimization, frontend performance; real metrics and improvements achieved
Practice Interview
Study Questions
Challenges Overcome and Learning
Significant technical or organizational challenges in your project, how you approached them, what you learned, how it shaped your approach going forward
Practice Interview
Study Questions
Retrospective Thinking and Iteration
What would you do differently now? What surprised you? How has your thinking evolved? What new knowledge would you apply?
Practice Interview
Study Questions
Team Dynamics and Collaboration on the Project
How you coordinated with team members (frontend/backend engineers, product, design), handled disagreements, ensured alignment, mentored others
Practice Interview
Study Questions
Technical Decision Making and Trade-offs
Why you chose specific technologies, databases, frameworks; trade-offs considered (speed vs. scalability, simplicity vs. flexibility); defending choices with reasoning
Practice Interview
Study Questions
Full-Stack Project Architecture and Integration
How you architected a full-stack feature: frontend state management, API design, backend logic, data persistence; how layers interact; front-end/back-end collaboration
Practice Interview
Study Questions
Frequently Asked Full-Stack Developer Interview Questions
A Java service in production shows steadily increasing heap usage over weeks and occasional OOMs. Describe a systematic root cause analysis plan using heap dumps, jmap/jstack, GC logs, async-profiler/flamegraphs, and how to identify retained object paths and suspicious classes. Describe short-term mitigations while fixing root cause and long-term practices to prevent recurrence.
Sample Answer
Framing
Heap usage climbing over weeks with occasional OOMs (Out Of Memory events, where the process is killed or fails because it exceeded available memory) is the classic slow-leak signature. The plan is: confirm the shape, capture evidence, trace what's retaining memory, then stop the bleeding while fixing the real cause.
1. Confirm the shape before diving in
Pull GC (garbage collection) logs/metrics over the multi-week window. Linear upward drift in heap-used-after-a-full-collection is a leak signature; a stable-but-high plateau close to the max heap is more likely a sizing problem than a leak.
2. Capture evidence: two heap dumps, not one
A heap dump is a full snapshot of every live object on the JVM (Java Virtual Machine) heap at one instant. Trigger one with jmap -dump:live,format=b,file=heap1.hprof <pid>, then repeat hours or days later under comparable load to get heap2.hprof. One dump alone cannot show growth; comparing two is what reveals which classes and object counts are actually increasing.
3. Trace retained object paths
Load both dumps into a heap analyzer (e.g. Eclipse MAT) and sort by RETAINED size (total memory that would be freed if that object were collected, including everything it exclusively keeps alive), not shallow size (the object's own size alone). For the top retainers that grew between the two dumps, follow "path to GC roots" to see exactly what reference chain is keeping them alive, common culprits are a static collection that's never trimmed, a ThreadLocal never cleared, or listeners registered but never removed.
4. Confirm with allocation profiling and thread state
Run async-profiler (a low-overhead sampling profiler for the JVM) in allocation mode (-e alloc) to get a flame graph of WHERE in the code the suspicious class is actually being allocated, complementing the dump's "what's retained" view with "where it comes from." Take jstack thread dumps around a slow-down or near-OOM moment to rule out request-scoped objects being pinned alive by a pile of blocked threads rather than a true static leak.
Short-term mitigations
- Increase heap size, or add capacity, as a stopgap that buys investigation time; it does not fix the root cause.
- Add a health-check-based rolling restart tied to a memory threshold (not a fixed schedule) so the process recycles before it OOMs.
- If growth correlates with a specific request type or feature flag (a toggle that turns a code path on or off without a deploy), consider a temporary circuit breaker (a safeguard that stops calling an overloaded dependency for a cooldown period instead of retrying forever) on that path.
Long-term prevention
- Bound every cache with a max size and a TTL (Time To Live, an expiration window), rather than a raw unbounded map.
- Add a leak-detection canary test that runs a fixed number of iterations and asserts heap returns to baseline.
- Alert on heap TREND (a sustained upward slope over days), not just on OOM itself, so this class of problem is caught in days instead of weeks.
Explain the difference between vertical scaling (scaling up: moving to a bigger machine) and horizontal scaling (scaling out: adding more machines) for a service or system. Define scale-up, scale-down, scale-out, and scale-in, and give concrete examples of each, such as resizing a VM instance versus adding more replicas behind a load balancer. Compare the two approaches on cost, fault tolerance and failure domains, operational complexity, downtime windows, and single-node limits, and describe what it takes to migrate a service from one approach to the other.
Sample Answer
Definitions
- Vertical scaling (scale up): giving a single machine more resources, more CPU, RAM, faster disk, for example resizing a VM from 4 vCPUs to 16. Scale down is the reverse, shrinking that same machine's resources when load drops.
- Horizontal scaling (scale out): adding more machines running the same workload behind a load balancer, for example going from 2 web server replicas to 10. Scale in is the reverse, removing replicas when load drops.
Comparison
- Cost: vertical scaling has a straightforward cost curve at small scale but gets disproportionately expensive at the high end, the biggest instance sizes carry a real premium per unit of resource. Horizontal scaling costs scale more linearly with replica count, though it adds the fixed cost of a load balancer and any coordination infrastructure.
- Fault tolerance and failure domains: a single vertically-scaled machine is a single point of failure by definition, if it goes down the whole service goes down with it. Horizontally-scaled replicas spread the same workload across multiple failure domains, so losing one replica degrades capacity but does not take the service down.
- Operational complexity: vertical scaling is operationally simple, resize and restart. Horizontal scaling requires the application to actually support running many instances at once (statelessness or shared state, load balancing, service discovery), real engineering work, not just an infrastructure change.
- Downtime windows: resizing a VM vertically commonly requires a restart, real downtime or at least a brief interruption, unless the platform supports live resizing. Adding or removing horizontal replicas is normally a zero-downtime operation, since existing replicas keep serving traffic while new ones join or old ones drain.
- Single-node limits: vertical scaling always hits a ceiling, the biggest instance the cloud provider offers, beyond which there is nowhere further to scale that one machine. Horizontal scaling has a much higher practical ceiling, since you can generally keep adding machines, though eventually something else, like a shared database, becomes the new bottleneck.
Migrating from vertical to horizontal
- Make the application stateless, or externalize whatever state it currently keeps in local memory/disk (sessions, local caches, uploaded files) to a shared store (a database, Redis, object storage), since horizontal scaling only works cleanly if any instance can handle any request.
- Put a load balancer in front of the service and validate health checks work correctly for it.
- Start with a small number of replicas alongside, not replacing, the existing large vertical instance, and gradually shift traffic while validating behavior is identical across instances.
- Once confidence is established, scale the replica count instead of the single instance's size going forward, and shrink or retire the original oversized instance.
How do you stay informed about what a function you regularly work with actually cares about and is measured on, even when you're not in the room for their planning?
Sample Answer
Direct answer
Build a standing information diet from what the partner function already produces for itself, its goals or planning document, the metrics it is measured on, and its retro or release notes, and pair that with a recurring informal check-in with one counterpart in that function. You are not trying to get invited into their planning meeting; you are trying to read what they optimize for, and occasionally confirm your read against a real person.
Structured elaboration
| Channel | Typical cadence | What it surfaces |
|---|---|---|
| Their goals or planning document (OKRs, roadmap) | Once per planning cycle | What they are formally accountable for this period |
| Dashboards or metrics they report on | Check periodically | What "good" looks like for them, in their own numbers |
| Retro notes, release notes, postmortems | As published | What is currently painful or top of mind for them |
| Recurring 1:1 with one counterpart | Biweekly or monthly | Informal context, upcoming priorities, translation of jargon |
| Occasional silent sit-in on their planning | A couple of times a year | Calibrates your read of the artifacts against how they actually talk about trade-offs |
The habit that ties these together: translate their metric into one sentence you could say back to them and have them agree it is accurate, then test that sentence the next time you talk. If you cannot state their current priority in a sentence they would sign off on, your information diet has a gap.
Worked example
Suppose you regularly partner with a support or customer-success function but are not in their planning. Their quarterly goals page (a document they publish for their own team) states the goal is "reduce median response time." Reading that before proposing a change that would meaningfully increase inbound volume lets you flag the likely trade-off to your counterpart ahead of launch, rather than finding out after the fact that you worked against their stated goal. The artifact told you what they were measured on; the counterpart conversation confirmed it was still current.
Trade-offs & pitfalls
- Relying only on artifacts risks reading a goal that is stale or aspirational and no longer reflects what the team is actually prioritizing day to day.
- Relying only on a single counterpart's opinion risks mistaking one person's take for the function's actual priority, especially if that person is not close to how the team's metrics are reviewed.
- A common miss: reading the dashboard but never validating the interpretation with anyone in that function, which produces confidently wrong assumptions that only surface when a decision already went the wrong way.
- The senior differentiator on an easy-sounding question like this is treating it as a standing habit built before you need it, rather than something you scramble to learn only after a conflict has already surfaced.
Write a Python function that detects whether an undirected graph (adjacency list Dict[int, List[int]]) contains any cycle. The function should return True if a cycle exists and False otherwise. Explain why you must track the parent node during DFS to avoid false-positive detection from immediate back-edges. Ensure O(|V|+|E|) time.
Sample Answer
Direct answer
In an undirected graph, DFS with a tracked parent argument detects a cycle by treating an edge to an already-visited node as a cycle, EXCEPT when that already-visited node is the immediate parent, since every undirected edge is stored twice (once in each endpoint's adjacency list), so walking straight back along the edge you just arrived on would otherwise look identical to a genuine cycle. Passing the parent explicitly and skipping exactly that one edge is what makes the check correct.
Structured elaboration
Why the parent check exists at all. An undirected edge (u,v) is represented as v∈graph[u] AND u∈graph[v], both directions stored. A DFS that goes from u to v will, when examining v's neighbors, immediately see u again, the edge it just came from. Without a parent check, this looks exactly like discovering an already-visited node, indistinguishable from a real cycle, even on a simple two-node graph with a single edge and no cycle at all. Recording the parent and explicitly skipping the edge back to it removes this false signal while leaving every genuine cycle (an edge into a visited node that is NOT the immediate parent) correctly detected.
Worked example
from typing import Dict, List
def has_cycle(graph: Dict[int, List[int]]) -> bool:
visited = set()
def dfs(u: int, parent: int) -> bool:
visited.add(u)
for v in graph.get(u, []):
if v == parent:
continue # the edge back to where we came from, not a cycle
if v in visited:
return True # a back-edge to an already-visited, non-parent node: a cycle
if dfs(v, u):
return True
return False
for node in graph:
if node not in visited:
if dfs(node, -1):
return True
return False
if __name__ == "__main__":
tree = {0: [1, 2], 1: [0, 3], 2: [0], 3: [1]} # a genuine tree, no cycle
print("tree (no cycle):", has_cycle(tree))
triangle = {0: [1, 2], 1: [0, 2], 2: [0, 1]}
print("triangle (cycle):", has_cycle(triangle))
disconnected_with_cycle = {0: [1], 1: [0], 2: [3, 4], 3: [2, 4], 4: [2, 3]}
print("disconnected, cycle only in 2nd component:", has_cycle(disconnected_with_cycle))
isolated = {0: [], 1: [], 2: []}
print("all isolated nodes:", has_cycle(isolated))
self_loop = {0: [0]}
print("self-loop:", has_cycle(self_loop))
def edges_from_adj(adj):
seen = set()
edges = []
for u in adj:
for v in adj[u]:
if (v, u) not in seen:
edges.append((u, v))
seen.add((u, v))
return edges
def brute_force_has_cycle(adj):
# Independent ground truth via plain union-find (correct here since
# this check is over UNDIRECTED edges, where union-find is valid).
nodes = list(adj.keys())
idx = {n: i for i, n in enumerate(nodes)}
parent = list(range(len(nodes)))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
for u, v in edges_from_adj(adj):
ru, rv = find(idx[u]), find(idx[v])
if ru == rv:
return True
parent[ru] = rv
return False
for name, g in [("tree", tree), ("triangle", triangle), ("disconnected", disconnected_with_cycle), ("isolated", isolated)]:
print(f"cross-check {name}:", has_cycle(g) == brute_force_has_cycle(g))
Output (actually executed with python3):
tree (no cycle): False
triangle (cycle): True
disconnected, cycle only in 2nd component: True
all isolated nodes: False
self-loop: True
cross-check tree: True
cross-check triangle: True
cross-check disconnected: True
cross-check isolated: True
Every DFS-with-parent result is cross-checked against a completely independently implemented union-find (correct for THIS undirected-only use case, unlike the directed case where union-find is unsound). The self_loop case (0: [0]) is correctly reported True: when DFS at node 0 examines its own self-loop edge, the neighbor is 0 itself, which is already in visited (added at the start of the call) and is NOT equal to parent (which is −1 for the root call), so it correctly falls through to the cycle-detected branch.
Complexity
Time O(∣V∣+∣E∣): each vertex is visited at most once (guarded by visited), and each edge is examined at most twice total (once from each endpoint), a constant factor that does not change the asymptotic bound. Space O(∣V∣) for visited and the recursion stack.
Edge cases
- Disconnected graph: the outer loop restarts DFS from every unvisited node, so a cycle in any component is found regardless of which component happens to be explored first.
- Isolated nodes (no edges at all): trivially no cycle, handled without any special-casing since the inner loop over
graph.get(u, [])simply does nothing. - Self-loop (
v == u): correctly detected as a cycle, as traced above; note thatv == uis NOT the same check asv == parent, so a self-loop is never accidentally treated as "the edge back to my parent" even on the very first call whereparentstarts at a sentinel value. - Parallel edges between the same pair (the same undirected edge listed twice): would be misreported as a cycle by this implementation, since the second occurrence of the neighbor is not equal to
parenton the SECOND time it is examined even though it is the same physical edge; a graph representation that can contain true parallel edges needs an explicit edge-id (not just a node-id) comparison to skip correctly, which this simple adjacency-list version does not attempt to solve.
Trade-offs and pitfalls
- Common mistake: omitting the parent parameter entirely and using a plain "is this neighbor visited" check, which reports every single edge in an undirected graph as a cycle, since the edge back to the immediate parent always looks like a revisit.
- Common mistake: comparing against a set or list of ALL ancestors instead of just the immediate parent. For an undirected graph this is unnecessary extra work, since only the single edge just traversed needs to be excluded, not the whole ancestor chain, that distinction (immediate parent only, versus the full ancestor set) is precisely what separates undirected cycle detection from the analogous directed case, where the full "on the current path" (gray/inStack) set genuinely is needed.
- Why this technique is undirected-specific. The parent-skip trick exists purely because undirected edges are stored bidirectionally; a directed graph never has this "walking back along the same edge looks like a revisit" problem in the first place, since a directed edge u→v has no automatic reverse entry, which is why directed cycle detection needs the gray/inStack (full active-path) mechanism instead of a simple single-parent exclusion.
You are a senior engineer faced with many teams disagreeing about a shared code style standard for new language adoption. Describe your leadership approach to reach a decision: how to gather input, weigh technical trade-offs, pilot the standard, communicate the change, and measure adoption while minimizing disruption.
Sample Answer
Direct answer
I treat this as a decision-making process problem, not a technical one: gather input broadly but make the actual call with a small accountable group, pilot the standard on a real team before mandating it org-wide, communicate the reasoning and not just the rule, and measure adoption with an honest, mechanically-enforced signal rather than assuming a written standard changes behavior on its own.
Structured elaboration
Gather input
Survey affected teams for their current conventions and actual friction, not just preferences, so the decision is grounded in real problems (for example, "our formatter conflicts with theirs when we share a monorepo") rather than taste. Reduce the debate to 2-3 genuinely competing proposals rather than open-ended bikeshedding; most style disagreements collapse to a small number of real axes, like import ordering or naming convention.
Weigh technical trade-offs
Favor whichever option has the strongest tooling support (an existing auto-formatter, mature editor integration, linter support) over one that's marginally nicer but manual to enforce, since manual enforcement is where standards quietly die. Weigh switching cost too: a team with a large existing codebase in the new language has more sunk cost in its current convention than a team just starting out.
Pilot before mandating
Pick one or two willing teams to adopt the standard for a real sprint or two, not a toy example, and explicitly ask what broke, what felt like friction, and what they'd change before finalizing anything.
Communicate the decision
Publish the reasoning, not just the rule: why this option over the alternatives, and what trade-offs were accepted. Give a clear timeline, a grace period, and name who to raise disagreement with.
Measure adoption and minimize disruption
Track adoption through the automated formatter or linter's own pass/fail rate in CI (continuous integration, the automated build/test pipeline) across repos, an honest signal since it's mechanically enforced rather than self-reported. Roll out with tools that fix code automatically instead of requiring manual compliance, the single biggest lever for minimizing disruption. Grandfather existing code with format-on-touch (only reformat files as they're naturally edited) rather than one disruptive mass reformat that breaks blame history and floods review queues.
Worked example
Three teams adopting a new backend language disagree on import-ordering and error-handling conventions. I run a two-week input-gathering round: two teams prefer style A, matching their existing microservices; one team already has 40,000 lines in style B, a shared library they don't want to rewrite. Rather than forcing a binary choice, I pick style A as the org standard, since it has better tooling support (an existing auto-formatter plugin), but scope the rollout as format-on-touch: new and touched files get reformatted automatically by CI, while untouched legacy files in the third team's library keep style B until they're naturally edited, with a linter configuration that doesn't flag the legacy files. I publish a short doc explaining the tooling reasoning and the grandfather policy, pilot on the first team for two weeks, adjust the auto-formatter's import-grouping rule after they report false-positive linter noise, then roll out to the other two teams. I track adoption as the percentage of touched files in each repo that pass the new formatter in CI, which climbs past 90% within a month without anyone manually reformatting anything.
Trade-offs and pitfalls
- Forcing a big-bang reformat of an entire existing codebase creates a disruptive diff that breaks
git blameand swamps review queues; format-on-touch avoids this at the cost of a longer period of inconsistency - Deciding by committee vote often produces a compromise nobody's tooling actually supports well; weigh tooling maturity heavily, not just preference counts
- Skipping the pilot and mandating org-wide immediately is the most common way this backfires, since the standard hasn't been tested against a real team's actual workflow
- Publishing a rule without publishing the reasoning breeds quiet non-compliance; people follow standards they understand the "why" of far more reliably
Implement an LRU cache in Python with get(key) and put(key, value) both running in O(1) time and a fixed capacity that evicts the least-recently-used entry on overflow. State the time and space complexity of each operation, then discuss what changes if the cache must be safe under concurrent access from multiple threads.
Sample Answer
Approach: Use Python's built-in collections.OrderedDict, which is a hash map that also maintains insertion/access order internally (backed by a doubly linked list under the hood) - move_to_end and popitem(last=False) give the O(1) reordering and eviction primitives directly, without hand-rolling the linked-list pointer surgery.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
if capacity <= 0:
raise ValueError("capacity must be positive")
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return -1
self.data.move_to_end(key) # mark as most-recently-used
return self.data[key]
def put(self, key, value):
if key in self.data:
self.data.move_to_end(key)
self.data[key] = value
if len(self.data) > self.capacity:
self.data.popitem(last=False) # evict least-recently-used
Key points: get and put are each O(1) amortized because OrderedDict maintains its internal doubly linked list alongside the hash table, so move_to_end and popitem(last=False) are O(1) list-splice operations, not O(n) scans. Capacity is enforced on every put by evicting the oldest entry once size exceeds capacity.
Complexity: Both get and put are O(1) time. Space is O(capacity), since the cache never holds more than capacity entries.
Edge cases: capacity 0 is rejected explicitly (a cache that can hold nothing is a degenerate case worth guarding); updating an existing key's value correctly refreshes its recency without growing the cache past capacity; requesting a missing key returns a sentinel (-1 here, following the common LeetCode convention) rather than raising, though a production cache would more idiomatically raise KeyError or return None.
Worked example / execution verification
cache = LRUCache(2)
cache.put(1, 'a')
cache.put(2, 'b')
print(cache.get(1)) # promotes 1 to most-recent -> 'a'
cache.put(3, 'c') # over capacity, evicts 2 (least-recently-used)
print(cache.get(2)) # -1, was evicted
print(cache.get(1)) # 'a', still present
print(cache.get(3)) # 'c', still present
Executed: output is a, -1, a, c - exactly matching the hand-traced sequence from the conceptual companion question (key 2 is correctly evicted because the get(1) call promoted 1 ahead of it, leaving 2 as the least-recently-used entry when 3 was inserted over capacity).
Trade-offs & pitfalls
OrderedDictis not thread-safe - concurrentget/putcalls from multiple threads need an external lock (or a striped-lock / lock-free design, at the cost of significant added complexity) if this cache backs a multi-threaded service.- Hand-rolling the hash-map-plus-doubly-linked-list structure explicitly (rather than relying on
OrderedDict) is worth being able to do too - some interviewers specifically want to see the pointer manipulation, sinceOrderedDictis "using a library that already solved it." - For very large caches, consider whether the per-entry overhead of a linked-list node plus hash-map entry (roughly 3-4x a raw value's memory footprint in CPython) is acceptable, or whether a more compact structure (e.g. a ring buffer indexed by insertion slot, at the cost of O(capacity) eviction scans) is a better fit for the memory budget.
In JavaScript, write a concise Service Worker fetch handler that implements a 'stale-while-revalidate' strategy for images: return cached image if present, kick off a background fetch to update the cache, and on cache miss fetch from network and store in cache. Use async/await, Cache API, and include a cache name constant and a TTL comment.
Sample Answer
Approach
Stale-while-revalidate means: return whatever is currently cached immediately (even if it might be out of date), and kick off a background fetch to refresh it for next time, never blocking the response on the network. The demo below uses the same minimal Cache API polyfill (caches.open, cache.match, cache.put) as any other Service Worker demo, so the real fetch-handler logic can be executed and its behavior verified directly rather than just described.
// Same minimal Cache API polyfill as the other Service Worker demo, so the
// real fetch-handler logic below can run and print its actual behavior here.
class MockCache {
constructor() { this.store = new Map(); }
async match(request) { return this.store.get(request) ?? null; }
async put(request, response) { this.store.set(request, response); }
}
class MockCacheStorage {
constructor() { this.caches = new Map(); }
async open(name) {
if (!this.caches.has(name)) this.caches.set(name, new MockCache());
return this.caches.get(name);
}
}
const caches = new MockCacheStorage();
const IMAGE_CACHE = 'images-v1'; // TTL is enforced by refetching on every
// read via revalidate(), not by a stored
// expiry: the cache never serves anything
// OLDER than "whatever the last background
// fetch returned", which is the point of
// stale-while-revalidate.
const networkLog = [];
async function fakeNetworkFetch(url) {
networkLog.push(url);
return { url, bytes: `image-bytes(${url})@${networkLog.length}` };
}
/**
* Real Service Worker fetch handler for images:
* - cache hit -> return the cached response immediately, refresh in the background
* - cache miss -> fetch from network, cache it, then return it
*/
async function handleImageFetch(request) {
const cache = await caches.open(IMAGE_CACHE);
const cached = await cache.match(request);
if (cached) {
// don't block the response on the network: fire-and-forget the refresh
fakeNetworkFetch(request).then((fresh) => cache.put(request, fresh));
return cached;
}
const fresh = await fakeNetworkFetch(request);
await cache.put(request, fresh);
return fresh;
}
async function main() {
console.log('Request 1 (miss, network fetch):', await handleImageFetch('/img/hero.png'));
console.log('network calls:', networkLog);
console.log('\nRequest 2 (hit, returns stale immediately, refresh queued):');
console.log(await handleImageFetch('/img/hero.png'));
// give the fire-and-forget refresh a tick to land
await new Promise((r) => setTimeout(r, 10));
console.log('network calls after refresh landed:', networkLog);
console.log('\nRequest 3 (hit again, now serves what the background refresh stored):');
console.log(await handleImageFetch('/img/hero.png'));
console.log('network calls:', networkLog);
}
main();
Output
Request 1 (miss, network fetch): { url: '/img/hero.png', bytes: 'image-bytes(/img/hero.png)@1' }
network calls: [ '/img/hero.png' ]
Request 2 (hit, returns stale immediately, refresh queued):
{ url: '/img/hero.png', bytes: 'image-bytes(/img/hero.png)@1' }
network calls after refresh landed: [ '/img/hero.png', '/img/hero.png' ]
Request 3 (hit again, now serves what the background refresh stored):
{ url: '/img/hero.png', bytes: 'image-bytes(/img/hero.png)@2' }
network calls: [ '/img/hero.png', '/img/hero.png', '/img/hero.png' ]
Key points
- Cache hit: return the cached response immediately (request 2 above returns the same bytes it had before), then fire off a background fetch without awaiting it in the response path,
.then(...)without a precedingawaiton the response. - Cache miss: no cached value to return yet, so this one request has to wait on the network, then gets cached for next time.
- TTL comment: there's no stored expiry timestamp here; freshness is enforced by refetching on every read via the background revalidation, so the cache never serves anything staler than "whatever the last background fetch returned."
IMAGE_CACHE = 'images-v1', the trailing version number is how you'd force a hard reset if the caching strategy itself ever changes.
Edge cases
- If the background revalidation fetch fails (network error, 500 from origin), the cached (now technically stale) image keeps being served indefinitely until a fetch eventually succeeds; that's usually the right default for images specifically, since a broken image is worse UX than a stale one.
- Two overlapping requests for the same image while a background revalidation is already in flight both read the currently cached value and both trigger their own revalidation fetch; harmless for images (idempotent, cache.put on the same key twice is fine) but worth flagging as duplicate network work under high concurrency.
You are choosing a load balancer for an API gateway that needs header-based routing, TLS termination, and WebSocket support. Would you pick a Layer 4 or Layer 7 load balancer, and why? Discuss how your choice affects session affinity, health checks, and autoscaling of the backend services.
Sample Answer
Direct answer
For an API gateway that needs header-based routing, TLS termination, and WebSocket support, choose a Layer 7 load balancer, typically a purpose-built API gateway built on an L7 proxy such as Envoy or NGINX. All three requirements only make sense above the transport layer: header inspection and TLS termination require parsing beyond TCP, and correct WebSocket support means handling the HTTP Upgrade handshake and then holding the resulting connection open as a long-lived stream rather than a short request/response cycle.
Structured elaboration
- Header-based routing needs the balancer to read the HTTP request line and headers, which is by definition an L7 operation; an L4 balancer only sees IP and port.
- TLS termination at the gateway centralizes certificate management and lets the gateway apply routing decisions to the decrypted request; an L4 balancer would have to pass TLS through untouched, which is incompatible with header-based routing.
- WebSocket support requires the proxy to recognize the
Upgrade: websockethandshake, respond correctly, and then treat the connection as a long-lived, low-request-rate stream instead of applying HTTP request timeouts to it.
Session affinity: L7 gateways can implement affinity via cookies or a header-based token, which survives client IP changes (mobile networks, corporate NAT). Prefer stateless backends with a shared session store over affinity where possible, since affinity re-couples scaling to specific instances: pinning a client's requests to one backend means that backend cannot be drained, replaced, or autoscaled away without either breaking that client's session or running a migration step first, unlike a stateless backend, which any instance can serve interchangeably. For a WebSocket connection specifically that coupling is even tighter, the connection itself is pinned to one backend process for its entire lifetime by the nature of a long-lived stream, so scaling in an instance mid-connection always terminates every WebSocket it is holding, whether or not cookie-based affinity is even in use.
Health checks: use active HTTP checks against a readiness endpoint, and make sure the check reflects the ability to accept a WebSocket upgrade, not just answer a plain GET. A backend that fails upgrades but passes a generic HTTP check will look healthy while breaking every WebSocket client.
Autoscaling: centralizing TLS and routing on the gateway makes it critical infrastructure, so it must scale independently of the backends and stay stateless (shared config, no local session state). Backend autoscaling should key off concurrent connections for WebSocket-heavy services, not just CPU, since an idle WebSocket connection holds a slot without generating CPU load.
Worked example
The same gateway often serves two very different endpoints. A /v1/search endpoint returns small JSON responses quickly and can use small buffers and short timeouts. A /v1/uploads bulk-ingestion endpoint accepts large file payloads and needs the gateway to stream the request body to the backend rather than fully buffering it first, plus a much longer idle timeout. Configuring both endpoints with the same defaults turns the L7 tier into exactly the kind of bottleneck it was chosen to avoid: the search path gets timeouts that are too generous, and the upload path either times out mid-transfer or forces the gateway to hold large payloads in memory. The fix is per-route configuration (buffering, body size limits, timeouts) rather than one global policy for the whole gateway.
Trade-offs & pitfalls
- Centralizing TLS and routing on one L7 tier makes it a high-value target for both attacks and outages; it must be horizontally scaled and kept stateless or it becomes the new single point of failure.
- WebSocket connections do not fit request-count or CPU-based autoscaling signals well, since an idle connection still occupies a slot; scale on concurrent connections too.
- Sticky sessions look like the easy fix for keeping a client's WebSocket routing consistent, but they reintroduce the coupling between traffic and specific instances that choosing L7 with a shared store was meant to avoid.
A postmortem is written, everyone nods along, and six months later a new team hits the same problem because nobody found the earlier write-up. How would you make incident learnings genuinely discoverable and get stakeholders to actually adopt postmortem-recommended changes, rather than leaving the findings as a static document nobody revisits?
Sample Answer
Direct answer
Converting postmortem findings into durable organizational knowledge means making them genuinely discoverable when someone needs them later, not just archived, and actively driving adoption of the recommended changes rather than assuming a written document alone will change anyone's behavior.
Structured elaboration
- Make it searchable, not just stored. Consistent tagging (by system, by failure category, by team) and a real search interface matter more than where the document technically lives; a postmortem nobody can find when facing a similar problem six months later has produced no lasting value regardless of how good the analysis was.
- Link forward, not just file away. Connect the postmortem to the runbooks, code, or design docs it should influence, so someone reading the runbook for a related system encounters the relevant lesson in context, rather than only finding it if they happen to search the postmortem archive specifically.
- Distribute, don't just publish. A regular digest of recent postmortems' key lessons (even a short one, shared org-wide or per relevant team) reaches people who wouldn't have gone looking, and repeated exposure is often what actually changes behavior, not a single document existing somewhere.
- Drive adoption of the recommended change actively, not passively. If a postmortem recommends a new practice (mandatory pre-deploy data tests, for example), treat rolling that recommendation out as its own project: identify a pilot team, demonstrate impact with real before-and-after data, and use that evidence to build the case for broader adoption, rather than assuming the recommendation alone will spread on its own merit.
- Periodically revisit and retire stale entries. Old postmortems referencing systems that no longer exist or practices that have since changed clutter the knowledge base and erode trust in search results; a light periodic review keeps the archive useful rather than just growing.
Worked example
A postmortem recommends mandatory pre-deploy data-validation tests after a bad data pipeline change silently corrupted downstream reports. Six months earlier, a similar (if less severe) incident had happened and been documented, but the postmortem sat unread and the recommendation was never adopted broadly. This time, instead of just filing the new postmortem, the team: tags it clearly under 'data pipeline' and 'validation gap,' links it directly from the data-pipeline team's onboarding docs and runbook, and pilots the recommended pre-deploy test requirement with one willing team first. After demonstrating the pilot caught two would-be incidents before they shipped, real evidence rather than a hypothetical, the team presents that data to engineering leadership and uses it to justify making the practice mandatory org-wide, with the earlier postmortem now cited as the founding case study in the org-wide rollout communication.
Trade-offs and pitfalls
The most common mistake is treating 'we wrote it down' as equivalent to 'we learned from it,' when in practice a document with no distribution, linking, or active adoption effort is functionally invisible to everyone except the person who wrote it. A second is over-investing in an elaborate knowledge-management system before addressing the more basic problem, which is usually that nobody is actively driving adoption of any given recommendation.
Write (or describe) how a LATERAL join can replace a correlated subquery when you need, for each row of an outer table, the top result from a related table (for example the most recent event per user, or the top-N per group). Explain why the LATERAL form is usually more optimizer-friendly than the equivalent correlated subquery.
Sample Answer
Direct answer. A LATERAL join lets a subquery on the right-hand side reference columns from a table on the left-hand side of the FROM clause, row by row, which is exactly what you need to compute "the top N related rows per outer row" without a correlated subquery in the SELECT list or a window function over the whole joined result.
Structured elaboration. A LATERAL subquery is evaluated once per row of whatever precedes it in the FROM clause, with that outer row's columns visible inside the subquery, similar in spirit to a correlated subquery but structured as a proper join rather than an expression in the SELECT list, which lets it return multiple rows and columns naturally, and lets the optimizer reason about it more like an ordinary join than an opaque per-row expression.
Worked example. I verified this with two customers and six orders (four for customer 1, two for customer 2), returning the top 3 orders by amount per customer:
SELECT c.customer_id, o.order_id, o.total
FROM customers c,
LATERAL (
SELECT order_id, total
FROM orders o
WHERE o.customer_id = c.customer_id
ORDER BY total DESC
LIMIT 3
) o
ORDER BY c.customer_id, o.total DESC;
This correctly returned customer 1's top three orders (80, 65, 50, correctly excluding their fourth, smaller order) and customer 2's two available orders (999, 10), confirming the LATERAL subquery's WHERE o.customer_id = c.customer_id correctly re-scoped to each outer row and its own ORDER BY ... LIMIT 3 correctly capped the result per customer, not globally across all customers.
Trade-offs and pitfalls. LATERAL is usually more optimizer-friendly than an equivalent correlated scalar subquery specifically because it's structured as a genuine per-row join the optimizer can index-nest efficiently (an index on orders(customer_id, total) makes each per-customer lookup cheap), rather than an opaque per-row expression the optimizer has less visibility into; it's also a natural fit for "top N per group" specifically because the LIMIT lives inside the LATERAL subquery, scoped per outer row, which a plain window function approach achieves differently (ranking every row, then filtering on rank) with a comparable but structurally different cost profile.
Complexity
With a supporting index on the inner table's join and sort columns, this executes as roughly (outer rows) times (a cheap, index-bounded lookup for N rows), which scales far better than materializing every related row and sorting them all before trimming to N.
Edge cases
An outer row with fewer than N matching inner rows (customer 2's two orders, in the example) correctly returns just those, with no error and no padding, which is worth confirming explicitly since a naive alternative implementation can sometimes mishandle that case.
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