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
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.
Tell me about a time you had to make a decision or a technical/product trade-off with incomplete information (partial data, tight deadline) that materially affected a project. What assumptions did you make, what risks did you knowingly accept, how did you validate the choice quickly (e.g., small experiments or instrumentation), how did you communicate the uncertainty to stakeholders, and how did you adjust once more information arrived?
Sample Answer
Direct answer
The discipline that matters under a genuine incomplete-information trade-off is not guessing well, it is making assumptions explicit and testable, choosing a validation method that produces a real signal within the time available rather than more analysis of the same partial data, and being upfront with stakeholders about which part of the decision is confidence and which part is a bet.
Structured elaboration
- Name the gap precisely: what specific data is missing and why it matters for this decision, not a generic "we don't have enough data."
- State assumptions as testable claims: instead of silently assuming behavior will match a past pattern, write it down, for example "we assume the new segment behaves like the existing segment within 20%," so it can later be checked against reality.
- Identify the risk being knowingly accepted: what it costs if the assumption is wrong, and whether that cost is reversible or a one-way door.
- Validate fast, not exhaustively: given a tight deadline, add lightweight instrumentation to an existing rollout, or run a small live test, such as a limited canary group (a small slice of real users used to test a change safely) or a short A/B test (comparing two versions on a small slice of traffic), rather than trying to fully backfill the missing data before deciding.
- Communicate uncertainty explicitly: state the decision, the assumption it rests on, and what would change the decision, instead of presenting a partial-data conclusion with false confidence.
- Adjust once more information arrives: define upfront what new data would trigger a revisit, a specific threshold or date, so the decision does not quietly calcify into "how we've always done it."
Worked example
Two days before a scheduled launch, a team needed to decide whether a new caching layer for a high-traffic page was safe to enable, but the only load-test data available came from a much smaller synthetic test that did not reflect real traffic patterns. Assumption made explicit: the cache hit rate under real traffic would be at least 70%, based on the synthetic test's 78% discounted by a conservative 8 points for real-world variability. Risk knowingly accepted: if the hit rate came in lower, latency on the page could regress rather than improve, but the change was reversible through a feature flag (a toggle that turns a change on or off without a new deployment), which made the risk acceptable to take.
Rather than spending the remaining two days building a more realistic load test from scratch, the team shipped the cache behind the flag to 5% of production traffic with hit-rate and latency instrumentation added specifically to catch the failure mode, and watched it for four hours. Stakeholders were told the launch would proceed on a canary with an explicit rollback trigger, a hit rate below 65% or worst-case latency at the 95th percentile (a common way of describing how slow the slowest 5% of requests are) worse than baseline, not told the cache was verified. Once the canary showed a 74% hit rate, within the assumed range, the team ramped to 100% and documented the actual figure for the next similar decision.
Trade-offs and pitfalls
Treating "we made an assumption" as an excuse rather than a commitment to check it lets the assumption silently become permanent. Over-investing the limited time in more analysis of the same incomplete dataset instead of getting new signal from a small live test wastes the exact time that matters most. Presenting the decision to stakeholders with more confidence than it deserves erodes trust the first time it turns out wrong. Skipping a predefined rollback trigger means that by the time the assumption is proven wrong, the change may already be too embedded to safely reverse.
In React, implement a small global counter store using Context + useReducer that supports increment, decrement, reset, and persistence to localStorage. Provide Provider, reducer, a useCounter hook, and explain how you lazily initialize from storage without causing hydration issues.
Sample Answer
Approach (brief)
Use Context + useReducer for global state. Lazily read localStorage when running in the browser; to avoid SSR/hydration mismatch, render with a safe default on the server and hydrate actual stored value in a useEffect after mount.
Code (Provider, reducer, hook)
import React, { createContext, useContext, useReducer, useEffect } from 'react';
const CounterContext = createContext(null);
const ACTIONS = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
RESET: 'RESET',
HYDRATE: 'HYDRATE'
};
function reducer(state, action) {
switch (action.type) {
case ACTIONS.INCREMENT: return { count: state.count + 1 };
case ACTIONS.DECREMENT: return { count: state.count - 1 };
case ACTIONS.RESET: return { count: 0 };
case ACTIONS.HYDRATE: return { count: action.payload };
default: return state;
}
}
// default used for SSR to avoid mismatches
const DEFAULT_STATE = { count: 0 };
export function CounterProvider({ children }) {
// lazy init safe for client; will return DEFAULT_STATE on server (no window)
const lazyInit = () => {
try {
if (typeof window === 'undefined') return DEFAULT_STATE;
const raw = localStorage.getItem('counter');
return raw ? { count: Number(raw) } : DEFAULT_STATE;
} catch {
return DEFAULT_STATE;
}
};
const [state, dispatch] = useReducer(reducer, DEFAULT_STATE, lazyInit);
// persist on client whenever count changes
useEffect(() => {
try { localStorage.setItem('counter', String(state.count)); }
catch {}
}, [state.count]);
// In SSR scenarios, you might prefer to re-hydrate after mount to avoid any
// potential mismatches (optional if lazyInit already returned DEFAULT on server)
useEffect(() => {
try {
const raw = localStorage.getItem('counter');
if (raw != null && Number(raw) !== state.count) {
dispatch({ type: ACTIONS.HYDRATE, payload: Number(raw) });
}
} catch {}
}, []);
const value = {
count: state.count,
increment: () => dispatch({ type: ACTIONS.INCREMENT }),
decrement: () => dispatch({ type: ACTIONS.DECREMENT }),
reset: () => dispatch({ type: ACTIONS.RESET })
};
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>;
}
export function useCounter() {
const ctx = useContext(CounterContext);
if (!ctx) throw new Error('useCounter must be used within CounterProvider');
return ctx;
}
Why this avoids hydration issues
- On server render, lazyInit returns DEFAULT_STATE (no window), so server and initial client SSR markup match.
- After mount (client-only), a useEffect reads localStorage and dispatches HYDRATE if needed, replacing the value without causing SSR mismatch warnings.
- Persisting in a separate effect keeps storage updates isolated from render.
Notes / Best practices
- Keep localStorage read/writes in try/catch to handle privacy modes.
- Consider JSON for complex state and throttling writes for high-frequency updates.
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.
Design a detection pipeline to flag fraudulent or fake orders (e.g., card testing, repeated cancellations) in real-time. Describe feature extraction, scoring model options, thresholds, human review workflow, and how you'd integrate blocking without harming legitimate users.
Sample Answer
Approach summary (Full‑stack perspective)
I’d build a low‑latency pipeline: ingestion → feature extraction → scoring → action + human review. Use event stream (Kafka), lightweight feature store (Redis) for recent-session aggregates, and a scoring microservice (Go/Node/Python) behind an HTTP/gRPC API for frontend/backend use.
Feature extraction (real‑time + historical)
- Real‑time: IP, device fingerprint, velocity (orders/min by card/IP/user), payment token reuse, shipping / billing mismatch, geolocation vs billing country, time of day.
- Session: mouse/keyboard heuristics from frontend, checkout duration.
- Historical: user/order lifetime risk, chargeback rate, cancel ratio, avg order amount.
Implement client JS to emit signals; server enriches with Redis and Postgres.
Scoring model options
- Deterministic rules for known fraud (card testing patterns).
- ML models: online logistic regression for speed; gradient boosting (LightGBM) for accuracy offline, exported as FAST‑API inference for real‑time. Include explainability scores (feature contributions).
Thresholds & actions
- Score < 0.2: allow. 0.2–0.6: soft‑challenge (2FA, email/phone verification, delayed fulfillment). >0.6: block/pending review. Calibrate using ROC, cost matrix; prefer precision for blocking.
Human review workflow
- Queue high‑risk orders to an internal dashboard (React) with model explanation, full audit trail, ability to approve/reject, add tags. Capture reviewer decisions back to training store.
Blocking without harming legit users
- Progressive enforcement: soft challenge before hard block; show clear UX reasons; allow appeal/verification. Use rate limiting and adaptive thresholds per cohort (new users stricter). Monitor false positives with metrics (FP rate, manual review load, conversion) and rollback via feature flags.
Monitoring & iteration
- Track latency, precision/recall, business metrics. Retrain regularly; implement shadow mode to test rules/models before enforcement.
Design a greedy compression scheme (Huffman coding) for a payload where some symbols are far more frequent than others. Explain why always merging the two lowest-frequency nodes first produces an optimal prefix-free code, and what breaks the argument if you merged in a different order.
Sample Answer
Direct answer
Huffman coding is optimal among prefix-free codes because repeatedly merging the two lowest-frequency nodes builds the encoding tree bottom-up in exactly the order that minimizes total weighted path length (each symbol's frequency times its code length, summed over all symbols). Merging any other pair first can strand a frequent symbol deeper than it needs to be, which strictly increases the encoded size.
Structured elaboration
Build procedure: put every symbol in a min-heap keyed by frequency; while more than one node remains, pop the two smallest, create a parent whose frequency is their sum, and push it back; the last remaining node is the tree's root. Assigning 0/1 to left/right edges yields prefix-free codes, since every symbol sits at a distinct leaf.
import heapq
from collections import Counter
def build_huffman_codes(freqs: dict[str, int]) -> dict[str, str]:
"""
freqs: symbol -> frequency (must have >= 2 distinct symbols).
Returns symbol -> binary code string.
"""
if len(freqs) < 2:
raise ValueError("need at least 2 distinct symbols for a prefix tree")
counter = 0
heap = []
for sym, f in freqs.items():
heapq.heappush(heap, (f, counter, sym))
counter += 1
while len(heap) > 1:
f1, _, n1 = heapq.heappop(heap)
f2, _, n2 = heapq.heappop(heap)
merged = (n1, n2)
heapq.heappush(heap, (f1 + f2, counter, merged))
counter += 1
_, _, root = heap[0]
codes: dict[str, str] = {}
def walk(node, prefix):
if isinstance(node, tuple):
walk(node[0], prefix + "0")
walk(node[1], prefix + "1")
else:
codes[node] = prefix or "0" # single-symbol edge case
walk(root, "")
return codes
def encoded_length_bits(freqs: dict[str, int], codes: dict[str, str]) -> int:
return sum(freqs[s] * len(codes[s]) for s in freqs)
message = "abracadabra"
freqs = dict(Counter(message))
print("frequencies:", freqs)
codes = build_huffman_codes(freqs)
for s in sorted(codes, key=lambda s: (-freqs[s], s)):
print(f" {s!r}: freq={freqs[s]} code={codes[s]} (len {len(codes[s])})")
huff_bits = encoded_length_bits(freqs, codes)
fixed_bits = len(message) * 3 # 5 distinct symbols -> ceil(log2(5)) = 3 bits fixed-width
print(f"Huffman total bits: {huff_bits}")
print(f"Fixed-width (3 bits/symbol) total bits: {fixed_bits}")
Output:
frequencies: {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}
'a': freq=5 code=0 (len 1)
'b': freq=2 code=110 (len 3)
'r': freq=2 code=111 (len 3)
'c': freq=1 code=100 (len 3)
'd': freq=1 code=101 (len 3)
Huffman total bits: 23
Fixed-width (3 bits/symbol) total bits: 33
Why the greedy order is optimal (exchange argument sketch): take any optimal prefix tree. Among its deepest leaves, if the two globally-lowest-frequency symbols aren't already siblings there, swap them into those two deepest sibling positions; that swap can only lower or keep equal the total weighted path length, since moving a low-frequency symbol deeper (and a higher-frequency one shallower) never increases the frequency-times-depth sum. So an optimal tree with this sibling property always exists. Once those two symbols are fixed as siblings, merge them into one "super-symbol" of combined frequency; the same greedy step is optimal on the resulting (n-1)-symbol problem by induction, which is exactly what "always merge the two smallest" does at every level.
What breaks with a different merge order: merging arbitrary (not-lowest) nodes can pull a high-frequency symbol away from the root and bury it several merge levels deep before anything forces it back up, inflating its code length instead of shrinking it.
Worked example
For the weighted path length WPL(T)=∑s∈Σf(s)⋅depthT(s), applying the wrong merge rule (always combining the two LARGEST nodes first, instead of the two smallest) to the same "abracadabra" frequencies produces a tree whose weighted path length is 37 bits, worse than the fixed-width 33-bit baseline and far worse than the correct Huffman tree's 23 bits: the frequent symbol 'a' (frequency 5) gets buried under extra merges instead of staying near the root.
import heapq
freqs = {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}
def weighted_path_length(node, depth=0):
if isinstance(node, tuple):
return (weighted_path_length(node[0], depth + 1)
+ weighted_path_length(node[1], depth + 1))
return freqs[node] * depth
# Correct order: merge the two SMALLEST nodes each step (standard Huffman).
counter = 0
heap = [(f, i, s) for i, (s, f) in enumerate(freqs.items())]
counter = len(heap)
heapq.heapify(heap)
while len(heap) > 1:
f1, _, n1 = heapq.heappop(heap)
f2, _, n2 = heapq.heappop(heap)
heapq.heappush(heap, (f1 + f2, counter, (n1, n2)))
counter += 1
_, _, optimal_root = heap[0]
print("optimal weighted path length (bits):", weighted_path_length(optimal_root))
# Bad order: at each step, merge the two HIGHEST-frequency nodes instead.
counter = 0
heap2 = [[f, i, s] for i, (s, f) in enumerate(freqs.items())]
counter = len(heap2)
while len(heap2) > 1:
heap2.sort(key=lambda x: -x[0]) # sort descending, take two largest
f1, _, n1 = heap2.pop(0)
f2, _, n2 = heap2.pop(0)
heap2.append([f1 + f2, counter, (n1, n2)])
counter += 1
_, _, bad_root = heap2[0]
print("bad-order (merge two largest first) weighted path length (bits):", weighted_path_length(bad_root))
Output:
optimal weighted path length (bits): 23
bad-order (merge two largest first) weighted path length (bits): 37
Complexity
Building the tree is O(nlogn) for n distinct symbols (n−1 heap merges, each
O(logn)); encoding a message of length m is O(m); memory is O(n) for the
tree/codebook.
Edge cases
- Ties in frequency: any tie-break among equally-small nodes preserves the optimal total
length (though it can change WHICH codeword a given symbol gets); canonical Huffman fixes a
deterministic tie-break so encoder and decoder agree, which matters for decodability, not for
optimality itself. - Single-symbol alphabet is a degenerate edge case: assign a length-1 code by convention,
since there is nothing to disambiguate. - Fewer than 2 distinct symbols:
build_huffman_codesexplicitly raisesValueErrorwhen
len(freqs) < 2, since a prefix tree needs at least two leaves to have anything to
disambiguate.
Trade-offs & pitfalls
- Static vs adaptive: this build requires knowing (or transmitting) the frequency table up front; if the true distribution drifts mid-stream, adaptive Huffman (the Vitter algorithm) or arithmetic/range coding track a moving distribution without a fixed prior codebook, at the cost of more per-symbol bookkeeping.
- Where Huffman itself falls short: Huffman is optimal only among prefix codes with INTEGER-length codewords per symbol; a symbol with true probability 0.9 "wants" roughly 0.15 bits under the Shannon bound, but no prefix code can give any symbol fewer than 1 bit. For very skewed, single-dominant-symbol distributions, arithmetic or range coding packs closer to the entropy bound than Huffman ever can.
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.
You must introduce a breaking change to an existing public REST API used by many frontends. Compare URI-based versioning (e.g., /v2/), header-based versioning (Accept header), and content negotiation. Propose a concrete deprecation and rollout plan that minimizes disruption, including timelines, compatibility layers, communication strategies, and how you would enforce and monitor migration progress.
Sample Answer
Compare approaches (pros/cons)
- URI versioning (/v2/) — Pros: explicit, easy for frontends, caches well, simple routing. Cons: duplicate endpoints, harder to evolve granularly.
- Header-based (Accept-version / custom header) — Pros: cleaner URIs, flexible per-client rollout. Cons: harder for browsers/caches, more client work, tooling friction.
- Content negotiation — Pros: standard for media-type differences, useful when payload shape changes. Cons: complexity, weaker caching, brittle with browsers.
Recommended choice
- Use URI-based versioning for this breaking change (clear and easiest for varied frontend clients).
Deprecation & rollout plan
- Week 0: publish design doc + migration guide, example client patches, and sample curl/JS snippets. Announce on changelog, developer portal, and email list.
- Week 1–4: expose /v2/ in parallel; keep /v1/ behavior unchanged. Add deprecation headers on /v1/ responses:
Deprecation,Sunset(RFC 8594) with sunset date = 90 days. - Day 30: enable analytics capturing client IP, User-Agent, auth token app id, and requested endpoint version. Provide dashboard showing % traffic still hitting /v1/.
- Day 45: rate-limit non-migrated clients (soft 429 thresholds) and expand targeted outreach to top clients.
- Day 75: start blocking non-migrated clients with informative 410 + migration link for remaining heavy hitters (after escalation).
- Day 90: remove /v1/.
Compatibility & rollout mechanics
- Implement a compatibility layer in API gateway to translate common v1->v2 requests for quick wins; log translation occurrences. Use feature flags/canary to roll changes to 10%, 50%, 100% traffic. Release SDK/JS client updates and PRs for major frontends.
Communication & support
- Migration guide with diffs, code samples, and automated tests. Weekly status emails, Slack channel for support, and office hours for top customers. Tag GitHub issues and provide sample patches.
Enforcement & monitoring
- Track metrics: % calls to /v1 vs /v2, error rates, 4xx/5xx, top client identifiers. Dashboards + automated alerts when /v1 usage > thresholds. For non-responders, use escalation: support tickets → account manager → temporary rate limits → block.
This plan minimizes disruption by providing clear URIs, compatibility translations, progressive enforcement, active communication, and observable metrics so migrations can be measured and accelerated.
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 boundary check validates that a value (an index, an offset, a size) falls within the range the code actually handles correctly, and it routinely catches real production bugs before they cause damage. Pick three DIFFERENT kinds of boundary bugs you've seen or can construct realistically, and for each: describe the bug it would cause if unchecked, the specific defensive check you'd add, and a unit test that would catch a regression if the check were later removed.
Sample Answer
Direct answer
A boundary check catches a specific class of bug (accessing an index, offset, or value outside the range the code actually handles correctly) at the moment it happens, instead of letting it silently produce wrong output or crash somewhere unrelated later; three concrete examples: array/list indexing, pagination offsets, and numeric limits.
Structured elaboration and worked examples
- Array indexing: the bug is an off-by-one or attacker-controlled index reading past the end of a buffer or list. The defensive check: validate
0 <= index < len(array)before accessing, raising a clearIndexError/custom exception instead of either crashing with a cryptic native error or, in an unsafe language, reading adjacent memory. A unit test:assert_raises(IndexError, get_item, [1,2,3], 5). - Pagination offsets: the bug is a negative or absurdly large
offset/limitfrom a client, which can either error confusingly deep in a SQL driver or, worse, silently return zero rows and look like 'no data' rather than 'bad request'. The defensive check: clamp or rejectoffset < 0and caplimitto a sane maximum (say 1000) before it reaches the query layer. A unit test:assert paginate(items, offset=-5, limit=10) raises ValueError. - Numeric limits: the bug is an integer overflow or an out-of-domain value (a negative quantity in an order, a percentage over 100) silently producing a nonsensical result instead of an error. The defensive check: validate the value's range explicitly before using it in a calculation. A unit test:
assert_raises(ValueError, apply_discount, price=100, percent=150).
Trade-offs and pitfalls
Each of these checks is cheap individually, but the value comes from applying them CONSISTENTLY at every place the boundary is actually crossed (every array access from external input, not just the ones you happen to remember); a single unguarded pagination endpoint added six months later by someone who didn't see this pattern reintroduces the exact bug class. Treat these as patterns to lint for or wrap in a shared utility function, not as one-off checks to remember individually.
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