Lyft Junior Backend Developer Interview Preparation Guide
Lyft's interview process for Backend Developer (Junior Level) consists of a recruiter screening phase, followed by a technical phone coding round, and multiple onsite rounds covering coding proficiency, system design fundamentals, and behavioral assessment. The process emphasizes real-time distributed systems, scalable architecture, database optimization, and team collaboration—reflecting Lyft's core business of real-time ride-matching and platform reliability.
Interview Rounds
Recruiter Screening
What to Expect
Initial screening call with a recruiter to assess background, motivations, and fit for the role. This combined recruiter call covers both initial screen and recruiter follow-up. The recruiter will verify your experience with backend technologies, confirm your availability, and gauge your enthusiasm for Lyft. They may ask about your salary expectations and relocation willingness. Success here moves you to the technical phone screen.
Tips & Advice
Be clear and concise about your backend experience; focus on projects where you built APIs, optimized databases, or worked with cloud platforms. Research Lyft's mission around urban mobility and express genuine interest. Prepare 1-2 brief stories showcasing your problem-solving and collaboration. Ask thoughtful questions about the team and role. Confirm technical requirements: you'll need a quiet environment and reliable internet for upcoming technical rounds.
Focus Topics
Availability and Logistics
Confirm your availability for upcoming rounds, discuss timeline expectations, and verify technical setup (quiet space, reliable internet, video call readiness).
Practice Interview
Study Questions
Motivation and Fit for Lyft
Express genuine interest in Lyft's mission and the specific role. Connect your experience or interests to real-time systems, scalability, or urban mobility challenges.
Practice Interview
Study Questions
Communication and Professionalism
Speak clearly, listen actively to questions, and ask clarifying questions when needed. Keep answers focused and avoid rambling.
Practice Interview
Study Questions
Background and Experience Summary
Clearly articulate your backend development experience, projects you've worked on, and the technologies you're proficient with (Node.js, Python, Java, databases, cloud platforms).
Practice Interview
Study Questions
Technical Phone Screen
What to Expect
A 45-60 minute live coding interview via video call with a Lyft engineer. You'll solve 1-2 medium-difficulty algorithm problems using a shared online editor (e.g., CoderPad). The interviewer evaluates your coding proficiency, problem-solving approach, ability to handle edge cases, and communication while coding. This filters for candidates with solid algorithmic foundations before onsite rounds. Common topics include array/string manipulation, linked lists, trees, and basic graph problems.
Tips & Advice
Start by clarifying the problem statement and constraints with the interviewer—this demonstrates structured thinking. Write clean, readable code with clear variable names. Think aloud as you solve; explain your approach before coding. Test edge cases (empty inputs, single elements, duplicates). After finishing, discuss time/space complexity and possible optimizations. If stuck, communicate your thought process and ask for hints rather than staying silent. Write in a language you're most comfortable with (Python is popular for interviews due to readability).
Focus Topics
Problem-Solving Communication
Clarify ambiguities, explain your approach before coding, think aloud, and walk the interviewer through edge cases and test scenarios.
Practice Interview
Study Questions
Code Quality and Best Practices
Write clean, readable code with meaningful variable names, proper indentation, and comments where necessary. Handle edge cases explicitly.
Practice Interview
Study Questions
Linked Lists and Tree Problems
Traversal, insertion, deletion, serialization, and tree/graph operations. Examples: reverse linked list, binary tree level order traversal, validate binary search tree.
Practice Interview
Study Questions
Algorithm Complexity Analysis
Understand and articulate time complexity (O notation) and space complexity of your solution. Identify bottlenecks and discuss optimizations.
Practice Interview
Study Questions
Array and String Problems
Problems involving searching, sorting, sliding windows, two-pointer techniques, and dynamic programming on arrays. Examples: longest substring without repeating characters, merge sorted arrays, product of array except self.
Practice Interview
Study Questions
Onsite Technical Round 1: Coding and Problem-Solving
What to Expect
A 60-minute technical interview (onsite or virtual) where you solve coding problems similar to the phone screen but potentially with a higher difficulty tier. You may face 1-2 problems or 1 deeper problem with follow-up optimization requests. The interviewer also assesses how you explain trade-offs, handle debugging, and adapt when the problem evolves. This round filters for consistent coding proficiency and the ability to think critically under pressure.
Tips & Advice
Pace yourself; it's better to solve one problem completely and correctly than two partially. Before coding, verify assumptions and ask clarifying questions (e.g., constraints on input size, expected output format, any specific language preferences). Write test cases as you code. If you get stuck, communicate openly—say something like 'I think the approach is X, but let me reconsider the edge case here.' After your solution works, proactively discuss optimizations or alternative approaches. Show you can refactor and improve your code.
Focus Topics
Debugging and Adaptive Thinking
If your solution has a bug, walk through it step-by-step with the interviewer; ask clarifying questions and adapt your approach without becoming defensive.
Practice Interview
Study Questions
Trade-offs and Optimization Discussion
After solving, discuss alternative approaches, time/space trade-offs, and how to optimize further. For example, trading memory for speed or using caching.
Practice Interview
Study Questions
Edge Case Handling and Testing
Identify boundary conditions (empty arrays, single elements, duplicates, negative numbers) and write test cases to verify your solution handles them.
Practice Interview
Study Questions
Intermediate Algorithm Problems
Medium to hard-level LeetCode-style problems: hash maps for counting/grouping, backtracking, dynamic programming, graph traversal (BFS/DFS), and greedy algorithms.
Practice Interview
Study Questions
Onsite Technical Round 2: Backend and Database Fundamentals
What to Expect
A 60-minute technical interview focused on backend-specific skills: SQL queries, database design, API design principles, and query optimization. You may be asked to write SQL queries to retrieve specific data, design a database schema for a given scenario, or explain RESTful API design principles. This round assesses your practical knowledge of the tools backend developers use daily. Common topics include JOINs, indexing, normalization, N+1 queries, and HTTP methods/status codes.
Tips & Advice
For SQL: write queries clearly, explain your approach, and think about performance (indexes, query plans). If asked to design a schema, start with the core entities and their relationships, then normalize. For API design, explain your reasoning for resource structure and HTTP methods. If discussing optimization, identify bottlenecks (e.g., full table scans, missing indexes) and propose solutions. Draw diagrams for schema design if helpful. Ask clarifying questions about scale and constraints before diving into solutions.
Focus Topics
Authentication and Authorization Basics
Understand token-based authentication (JWT), API keys, session management, and role-based access control (RBAC) at a junior level. Know when and how to apply them.
Practice Interview
Study Questions
RESTful API Design Principles
Design APIs using proper HTTP methods (GET, POST, PUT, DELETE), status codes (200, 201, 400, 404, 500), and resource-oriented URL structures. Discuss request/response formats and error handling.
Practice Interview
Study Questions
N+1 Query Problem and Performance Bottlenecks
Identify and avoid N+1 query issues; understand query performance anti-patterns and solutions like eager loading, caching, and query batching.
Practice Interview
Study Questions
Database Design and Normalization
Design normalized database schemas with appropriate entities, relationships, and constraints. Understand primary keys, foreign keys, and trade-offs between normalization and denormalization.
Practice Interview
Study Questions
SQL Query Writing and Optimization
Write efficient SQL queries using SELECT, JOIN, GROUP BY, ORDER BY, subqueries, and common table expressions (CTEs). Understand indexes, EXPLAIN plans, and query optimization techniques.
Practice Interview
Study Questions
Onsite Technical Round 3: System Design and Scalability
What to Expect
A 60-minute system design interview where you design a real-time backend system for a ride-sharing or ride-matching scenario. The interviewer will guide you through requirements, and you'll sketch out architecture components, database choices, APIs, and trade-offs. For a junior developer, the focus is on understanding basic distributed system concepts, thinking about scalability, and articulating design choices clearly. You won't be expected to design a production system alone, but rather show foundational architectural thinking.
Tips & Advice
Start by asking clarifying questions to understand requirements: scale (users, requests per second), latency targets, key features. Outline your approach on a whiteboard or shared document before diving deep. Draw components: API gateway, application servers, databases, cache, message queues. Explain your choices for each component. Discuss trade-offs (consistency vs. availability, latency vs. throughput). For Lyft-relevant topics like real-time updates, mention WebSockets or server-sent events (SSE). Reference technologies like Kafka for event streaming, geospatial databases (PostGIS) for location queries, and Redis for caching. Focus on clarity and reasoning over perfection.
Focus Topics
System Design Communication and Trade-offs
Articulate your design choices clearly, explain trade-offs (consistency vs. availability, cost vs. performance), and be open to feedback and iteration from the interviewer.
Practice Interview
Study Questions
Real-Time Communication and Event Streaming
Discuss WebSockets for live updates, server-sent events (SSE), and message queues (Kafka) for asynchronous event processing. Understand latency implications.
Practice Interview
Study Questions
Geospatial Database and Location Queries
Understand spatial indexing, proximity queries, and tools like PostGIS for finding nearby drivers/riders. Discuss trade-offs in accuracy and performance.
Practice Interview
Study Questions
Database and Caching Strategy
Choose appropriate databases (SQL for transactional data, NoSQL for scale), caching layers (Redis), and explain trade-offs. Discuss consistency models if relevant.
Practice Interview
Study Questions
Ride-Hailing/Matching System Architecture
Design a backend system for matching drivers and riders in real-time. Include API design, matching algorithm considerations, real-time communication (WebSockets), and geospatial components.
Practice Interview
Study Questions
Scalability and Performance Considerations
Discuss handling peak load, sharding strategies (e.g., by geography or ride ID), caching patterns, and database optimization for large-scale systems.
Practice Interview
Study Questions
Onsite Behavioral Round
What to Expect
A 45-60 minute conversation with a Lyft engineer or manager focusing on teamwork, conflict resolution, learning, and alignment with Lyft's values. The interviewer will ask behavioral questions using the STAR format (Situation, Task, Action, Result) to understand how you've handled challenges, collaborated with teammates, and grown professionally. This round assesses cultural fit, communication skills, and your ability to work in a team-oriented environment.
Tips & Advice
Prepare 4-5 concrete stories from your past work or projects using the STAR method: Situation (context), Task (your responsibility), Action (what you did), Result (measurable outcome). Practice telling these stories concisely (2-3 minutes each). Tailor stories to themes like teamwork, learning from failure, handling disagreement, and delivering results. Listen carefully to each question and answer it directly; avoid long tangents. Show genuine interest in how Lyft approaches these values. Ask thoughtful questions about team dynamics and growth opportunities. Be authentic—interviewers can tell when answers are rehearsed versus genuine.
Focus Topics
Alignment with Lyft's Mission and Values
Express genuine interest in Lyft's mission (urban mobility, reducing congestion, supporting driver earnings). Connect your values and work style to Lyft's approach.
Practice Interview
Study Questions
Taking Initiative and Ownership
Share an example where you took on a task beyond your immediate responsibility, solved a problem proactively, or improved a process. Show ownership even at junior level.
Practice Interview
Study Questions
Learning from Failure and Growth Mindset
Describe a time you failed, what you learned, and how you applied the lesson. For junior developers, this could be a missed deadline, a bug you introduced, or a poor design choice you later refactored.
Practice Interview
Study Questions
Handling Conflict and Disagreement
Share a story about disagreeing with a teammate or manager, how you approached it constructively, and the resolution. Focus on listening, finding common ground, and respecting different perspectives.
Practice Interview
Study Questions
Teamwork and Collaboration
Discuss a time you worked effectively with teammates, communicated across functions (e.g., frontend, DevOps), or helped a colleague. Use STAR to show your role and impact.
Practice Interview
Study Questions
Frequently Asked Backend Developer Interview Questions
Explain the general trade-off between trading memory for speed and vice versa: precomputing/caching a result versus computing it on demand. Give a concrete example (a lookup table, a materialized aggregate) and describe the decision criteria - update frequency, staleness tolerance, and available memory - that determine which way to lean.
Sample Answer
Direct answer: The core trade-off is: spend memory now (precompute, cache, or store a lookup table) to save time later, or spend time recomputing on demand to save memory. Which way to lean depends on three factors: how often the result is reused, how tolerant the system is of stale/out-of-date results, and how much memory is actually available.
Structured elaboration
Classic precompute-vs-on-demand examples:
- A lookup table of precomputed values (e.g. factorials up to 1000, or a static configuration derived from rarely-changing source data) trades O(table size) memory for O(1) lookup, versus recomputing each value in whatever time the computation naturally takes.
- A cache of expensive function results (e.g. a memoization cache, or an application-level cache in front of a slow downstream service) trades memory for avoiding repeated work, valuable when the SAME inputs recur.
The decision criteria:
- Reuse frequency: if a value is computed once and never needed again, caching it wastes memory for no benefit; if it's requested repeatedly, caching pays for itself quickly.
- Staleness tolerance: if the underlying data changes and the cached/precomputed value must reflect changes promptly, you need invalidation logic (itself a real engineering cost) or must accept some staleness window.
- Available memory: precomputing an entire table only works if it fits in your memory budget - for a combinatorially large input space, on-demand computation (possibly with a bounded LRU (least-recently-used) cache for the hot subset) is the only option.
Worked example
Consider a service computing an expensive per-user recommendation score, requested on average 5 times per user session. Precomputing (caching) the score after the first computation means requests 2-5 are O(1) lookups instead of paying the full computation cost again - a 5x reduction in total compute for that session, at the cost of holding the cached score in memory for the session's duration. If instead each user only ever requested the score ONCE, caching would add memory overhead (allocating and eventually evicting a cache entry) with zero reuse benefit - the "precompute and cache" decision is only a net win when the reuse count exceeds roughly the overhead ratio of caching versus recomputing, which for a cheap computation might mean caching isn't worth it at all.
Trade-offs & pitfalls
- Caching introduces a NEW correctness concern (staleness/invalidation) that a pure recompute-on-demand approach doesn't have - "correct but occasionally slow" is sometimes preferable to "fast but occasionally wrong," depending on the domain (financial data usually can't tolerate staleness; a recommendation score usually can).
- Precomputing a full table only makes sense when the input space is small/bounded enough to enumerate - for an unbounded or combinatorially large input space, you need either an on-demand approach or a BOUNDED cache (with an eviction policy) covering just the hot subset.
- The "free lunch" case is when precomputation happens once, amortized across many users/requests, and the underlying data changes rarely (e.g. compiling a regex once at startup rather than on every request) - here there's essentially no downside, which is why this specific pattern (compute-once-at-startup) is nearly always a correct default when applicable.
Explain the difference between a stack and a queue and give a concrete example where each is the right choice. Then show how you would implement a queue using only two stacks (or a stack using only queues), and give the amortized cost per operation.
Sample Answer
Direct answer
A stack is last-in-first-out (LIFO): the most recently added item comes out first. A queue is first-in-first-out (FIFO): items come out in the order they arrived. Use a stack when you need to undo or backtrack in reverse arrival order, such as a browser's back button or a function call stack; use a queue when arrival order must be preserved, such as a task scheduler or a print spooler. You can build a queue out of two stacks: push is O(1) worst case, and pop is amortized (averaged over a sequence of operations) O(1) because each element only ever moves between the two stacks once over its lifetime.
Structured elaboration
| Stack (LIFO) | Queue (FIFO) | |
|---|---|---|
| Order returned | Most recent first | Oldest first |
| Concrete example | Undo history, expression parsing, recursive call stack | Print queue, request processing, breadth-first search frontier |
Queue from two stacks. Keep an in_stack that absorbs pushes and an out_stack that serves pops. Pushing always goes to in_stack in O(1). When a pop or peek is requested and out_stack is empty, drain all of in_stack into out_stack; this reverses the order, so the oldest element (which was at the bottom of in_stack) ends up on top of out_stack, ready to be returned first.
Why the amortized argument holds. Use the aggregate method: over any sequence of n operations, each element is pushed onto in_stack exactly once (cost 1), moved from in_stack to out_stack at most once in its lifetime (cost 1), and popped from out_stack exactly once (cost 1). No element is ever moved more than that, so the total work across the whole sequence is bounded by a constant multiple of n, which is what "amortized O(1) per operation" means, even though any single pop that triggers the drain costs O(n) by itself.
Worked example
class QueueFromStacks:
def __init__(self):
self.in_stack: list[int] = []
self.out_stack: list[int] = []
def push(self, x: int) -> None:
self.in_stack.append(x)
def _transfer(self) -> None:
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
def pop(self) -> int:
self._transfer()
return self.out_stack.pop()
def peek(self) -> int:
self._transfer()
return self.out_stack[-1]
if __name__ == "__main__":
q = QueueFromStacks()
q.push(1)
q.push(2)
q.push(3)
seq = [q.pop(), q.peek()]
q.push(4)
seq += [q.pop(), q.pop(), q.pop()]
print(seq)
Running this prints [1, 2, 2, 3, 4]. The first pop() triggers a drain (in_stack [1,2,3] becomes out_stack [3,2,1], top popped is 1); peek() then reads 2 for free from the already-drained out_stack; pushing 4 goes straight to in_stack without disturbing out_stack; the remaining pops (2, 3) come from out_stack, and the last pop (4) triggers a second drain since out_stack had emptied.
Complexity
| Push | Pop / peek | |
|---|---|---|
| Worst case (single call) | O(1) | O(n) |
| Amortized (over n calls) | O(1) | O(1) |
Space: O(n) total across the two internal stacks, since every pushed element lives in exactly one of them at any time (no extra space is used beyond storing the n elements themselves).
Edge cases
- Calling
pop()orpeek()on an empty two-stack queue: in the reference implementation,_transfer()leavesout_stackempty when both stacks are empty, sopop()'sself.out_stack.pop()andpeek()'sself.out_stack[-1]both raise an unhandledIndexErrorinstead of failing cleanly. Guard this explicitly, for exampleif not self.in_stack and not self.out_stack: raise IndexError("pop from empty queue")before touchingout_stack, so the caller gets a clear, intentional signal rather than an incidental one. - A single push followed immediately by a pop: the drain moves that one element from
in_stacktoout_stackand it is returned, leaving both stacks empty again, which is the state the empty-queue guard above must handle correctly on the next call.
Trade-offs & pitfalls
The most common confusion is treating "amortized" as "always fast": a single pop can still cost O(n) when it triggers the drain. Note the asymmetry with building a stack out of a single queue by rotating on every push (dequeue-then-requeue the previous elements so the newest sits at the front): that rotation happens on every single push, not just occasionally, so it is genuinely O(n) per push with no amortization to appeal to, unlike the two-stack construction above where the expensive transfer is rare and each element only ever pays for it once.
Two teams each blame the other after a shared-service outage: one insists a dependency's configuration change caused it, the other insists increased load from the first team was the real cause. You are asked to lead the postmortem and rebuild trust between the teams. How do you run the review, reach a fact-based conclusion, and secure buy-in on remediation from both sides?
Sample Answer
Direct answer
When two teams each blame the other after a shared outage, the facilitator's job is to separate 'what does the evidence show' from 'who is at fault,' and to run the discussion so both teams contribute evidence to a single shared timeline rather than defending competing narratives. This usually means gathering data from both sides before the meeting, framing the discussion around the timeline rather than either team's story, and being explicit that the goal is a joint fix both teams commit to, not a verdict on whose configuration change or whose load caused it.
Structured elaboration
- Before the meeting: pull metrics, logs, and change history from both teams independently, and build a single combined timeline that includes both teams' events (the config change AND the load increase, with exact timestamps), so the meeting starts from shared facts instead of each side's framing of the story.
- Framing: state explicitly at the start that the goal is a joint understanding of what happened and what to change, not deciding which team was 'right.' Reiterate that even if one team's change was the proximate trigger, the fact the system as a whole had no safeguard against that class of change is the real gap.
- In the discussion: ask each team to walk through their own timeline of events with evidence, not assertions, then look together at where the timelines intersect. Often the honest finding is that BOTH factors mattered (the config change was the trigger, but the system had no capacity headroom to absorb even a modest load increase on top of it), which is a more useful and more accurate conclusion than picking one side.
- Reaching resolution when the evidence itself is ambiguous: if the two hypotheses are genuinely both plausible and the data doesn't cleanly distinguish them, say so explicitly in the writeup rather than forcing a false consensus, and define what additional instrumentation or a follow-up experiment would resolve the ambiguity next time.
- Securing buy-in: end with action items owned by BOTH teams, not just one, so neither team can read the outcome as 'we were blamed and they weren't.'
Worked example
Team A's dependency config change and Team B's traffic increase happened within minutes of each other before a shared-service outage. The facilitator's combined timeline shows the config change reduced the service's effective connection pool size at 14:02, and Team B's traffic, itself normal and within historical range, arrived at 14:04 and exhausted the now-smaller pool. Neither event alone would have caused the outage: normal traffic against the old pool size would have been fine, and the smaller pool alone, without the traffic bump, might have gone unnoticed for a while. The joint conclusion: the real gap is that the shared service has no automated alert or gate when a config change materially reduces its capacity headroom. Action items: Team A adds a pre-deploy check that flags capacity-reducing config changes above a threshold, and the platform team (not either disputing team) adds monitoring on effective headroom versus recent traffic patterns, so future changes like this are caught automatically rather than depending on either team noticing.
Trade-offs and pitfalls
The most common failure is the facilitator implicitly picking a side, often by unconsciously giving one team's narrative more airtime, which the other team notices and which damages trust in the process going forward. A second is forcing a single, tidy root cause when the honest finding is that multiple factors from multiple teams combined; naming that clearly, with joint ownership of the fix, produces a more durable resolution than a false consensus.
An EXPLAIN ANALYZE shows a hash join spilling to disk (temp files). What causes a hash join to spill, how do you confirm that is actually happening from the plan output, and what are your options (query-level and configuration-level) for avoiding it?
Sample Answer
Direct answer. A hash join spills to disk when the hash table being built from one input doesn't fit inside the memory budget allotted to that operation, forcing the engine to partition both inputs and process them in batches with intermediate temp files; you confirm it from the plan by looking for an explicit batch or spill indicator alongside a jump in actual time relative to the row counts involved.
Structured elaboration. The build side of a hash join needs to fit (or be partitioned to fit) within the per-operation memory setting. When it doesn't, the engine splits both the build and probe inputs into multiple partitions small enough to fit in memory one at a time, writing the overflow to temporary disk files and reading them back in multiple passes. This isn't a bug: it's a graceful degradation that keeps the hash join correct at any input size, just at a real I/O and CPU cost.
To confirm this is happening (not, say, a slow input scan), most engines with detailed EXPLAIN ANALYZE output will explicitly report something like a batch count greater than one, or temp bytes/files written, directly on the hash join node; a hash join whose actual time is dramatically larger than a rough (rows times per-row cost) estimate, with no such explicit flag, is worth checking for spill via whatever temp-file or memory-usage view your engine exposes as a secondary confirmation.
Options to prevent or reduce spilling: increase the per-operation memory setting, but be conscious that this is a per-connection or per-query allowance, and raising it broadly can multiply total memory usage under concurrency; reduce the build side's row count with better filtering before the join (an earlier predicate or a narrower projection that lets more rows fit per memory unit); or reconsider whether the SMALLER of the two inputs is actually the one being chosen as the build side, since a misjudged build side is often the actual root cause rather than the memory setting itself.
Worked example. A feature-computation job whose hash join build side was under-estimated at 100,000 rows but turned out to be 5 million rows would very plausibly spill under a memory setting sized for the smaller estimate; fixing the underlying cardinality estimate (so the optimizer picks a bigger memory allocation, or a different join algorithm and build side entirely) often resolves this more durably than simply raising memory limits.
Trade-offs and pitfalls. Raising memory settings is the fastest fix but the least targeted: it helps this one query at the cost of every concurrently-running query on the instance potentially claiming more memory too, which can create its own resource-pressure problems under load. Prefer fixing the underlying cardinality estimate or the build-side size when that's the actual root cause.
What is an Architecture Decision Record (ADR), and what should one contain so a future maintainer understands not just what was decided but why (including the alternatives that were rejected)? Sketch a minimal template.
Sample Answer
Direct answer. An Architecture Decision Record is a short, durable document capturing ONE significant design decision at the moment it was made: what was decided, why, what alternatives were considered and rejected, and what constraints/trade-offs shaped the choice -- so a maintainer years later understands the REASONING, not just the resulting code.
Why this matters for refactors specifically
Code shows WHAT was built; it rarely shows what was DELIBERATELY NOT built and why. A future engineer looking at a seemingly-odd design choice (why isn't this using the 'obvious' simpler approach?) has no way to know whether that was a considered trade-off or an oversight, without some record of the reasoning -- which is exactly the gap ADRs fill, and exactly the kind of context a comment in the code often can't carry gracefully.
A minimal template
# ADR-014: Extract the pricing calculation into a standalone service
## Status
Accepted (2026-06-01)
## Context
Pricing logic was duplicated across three services (checkout, admin,
reporting), causing a class of bugs where a pricing rule change was
applied in two of three places (INC-118, INC-142).
## Decision
Extract pricing into a dedicated internal service with a versioned API,
rather than a shared library, so all three consumers always call a
single source of truth at runtime instead of each bundling a copy.
## Alternatives considered
- Shared library: rejected because it still requires all three services
to redeploy in lockstep for a pricing rule change to take effect
everywhere, which was the actual mechanism behind the past incidents.
- Leave duplicated, add a consistency test: rejected as treating the
symptom, not the cause; still requires perfect discipline going forward.
## Consequences
Adds a network dependency and latency to pricing calculation; accepted
as a deliberate trade-off given the incident history above.
What makes it useful, not just ceremony
- Recording REJECTED alternatives and WHY is often more valuable than the decision itself -- it preempts a future engineer re-proposing the shared-library approach without knowing it was already considered and specifically found insufficient.
- Keep it SHORT (this fits on one screen) -- an ADR that takes 20 minutes to read gets skipped; the goal is a quick, durable reference, not exhaustive documentation.
- Store ADRs IN the repository (a simple
docs/adr/folder, numbered sequentially) so they're versioned alongside the code they explain and discoverable by anyone browsing the codebase, not buried in a separate wiki nobody remembers to check.
Trade-offs and pitfalls
- Writing an ADR for every trivial decision drowns the genuinely significant ones in noise -- reserve them for decisions that were non-obvious, cost real effort to make, or that a reasonable future engineer might otherwise want to revisit or second-guess.
- An ADR is a point-in-time record, not a living document -- if a later decision supersedes it, write a NEW ADR that references and supersedes the old one, rather than editing history to make it look like the current decision was always the plan.
Tell me about a time you had to deliver bad news to stakeholders, like a delay, a budget cut, or a data error. How did you structure the conversation, what did you propose to mitigate the impact, and what was the outcome?
Sample Answer
Direct answer
Lead with the headline, not the buildup: tell people what happened and what it means for them before you explain how it happened. Then be explicit about what you're doing about it and by when. Stakeholders forgive a mistake much faster than they forgive finding out about it late, or getting a vague answer about what happens next.
Structured elaboration
- Verify before you communicate. Confirm scope and impact so your first message is accurate, not something you have to correct twice.
- Lead with impact, not mechanism. Open with what's affected and roughly how much, before the root cause.
- Explain the cause briefly and own it. A short, factual explanation, without over-apologizing or deflecting blame onto a tool or another team.
- Separate the short-term fix from the long-term prevention. What you're doing right now to correct the immediate problem, and separately, what changes so it doesn't recur.
- Give a concrete next checkpoint. A specific time you'll update them, not "soon."
Worked example
I found a data pipeline bug that had undercounted a meaningful chunk of the prior month's reported revenue for two product lines, the kind of number that gets read out in an executive review. I confirmed the affected reports and the rough scale of the error before saying anything to anyone. I called a short meeting with the Sales Director, the Finance lead, and the Head of Revenue Operations, opened with what was wrong and which numbers were affected, then explained the cause (an ETL, extract-transform-load, job had silently skipped a data partition after a schema change), and laid out the plan: reprocess the missing data and issue corrected dashboards the same business day, and separately, add an automated check on the pipeline so a skipped partition triggers an alert instead of a silent gap. I took ownership of the miss rather than framing it as a tooling problem.
Trade-offs and pitfalls
Moving fast to reassure people can tempt you to promise a number or a fix time before you've actually verified it, which turns one bad-news conversation into two. Leading with impact works, but if you skip the "here's exactly what I'm doing about it" part, impact-first reads as an announcement of a problem rather than ownership of one. And the long-term fix matters more than it feels like in the moment: stakeholders remember whether the same class of mistake happens again far more than they remember the apology.
How do you keep a cross-functional team aligned and moving when the people involved are spread across time zones with little or no overlap in working hours?
Sample Answer
Direct answer
Keep alignment across time zones with three levers: shrink what actually needs real-time overlap by defaulting to async updates on a fixed template, protect a small deliberately scheduled overlap window for anything that truly needs live discussion, and make handoffs explicit in writing so context transfers cleanly across the boundary instead of depending on someone's memory.
Framework
Reduce dependence on overlap. Default to async status updates on a fixed cadence, and use written decision docs rather than requiring a live meeting for every decision. Most updates don't need a room, only genuinely ambiguous or high-stakes calls do.
Protect a deliberate overlap window. Negotiate a recurring block, even a short one, and rotate who takes the inconvenient time so the burden doesn't always fall on the same region.
Make handoffs explicit. When work crosses a time-zone boundary, produce a short written artifact rather than relying on a quick chat message. This matters most in ops-heavy, always-on contexts.
Worked example
Consider an on-call rotation providing 24/7 production coverage across three time zones (for example [Region A], [Region B], and [Region C]), where the two outer regions have little or no live overlap with each other.
- Shadow and overlap periods: the incoming region's on-call shadows the outgoing region's on-call for a short deliberate window at the shift boundary, even 15 to 30 minutes, to ask questions live before the outgoing engineer signs off.
- Written handoff template: a standard document filled at every handoff covering open incidents, any systems in a degraded state, changes deployed in the last shift, and explicit 'known risk' or 'do not touch' notes.
- Escalation expectations: a written policy defining what counts as page-worthy versus a handoff note, who the secondary on-call is in each region, and how long the incoming engineer has to acknowledge before it auto-escalates.
Result: even with zero live overlap between two of the three regions, the written handoff plus the short shadow window from the middle region means each incoming on-call starts already briefed, instead of reconstructing state from raw logs.
For non-ops roles the same mechanism applies with a different artifact, for example a design or product handoff might be a written decision log plus a recorded walkthrough rather than an incident handoff, but the principle (explicit written handoff over a live conversation) is the same.
Trade-offs and pitfalls
- Repeatedly scheduling occasional syncs at painful hours burns out whichever time zone draws the short straw. Rotate it deliberately.
- Async-only breaks down for genuinely ambiguous or high-stakes decisions. Some live channel for true emergencies still has to exist.
- A handoff template that's too heavy gets skipped under time pressure. Keep it short enough to fill in within a few minutes.
- Assuming a chat message counts as a handoff is the actual failure mode this whole approach is designed to prevent. The structured artifact is the point, not the tool it's written in.
You're designing a marketplace reservation system where users can browse listings, hold inventory, and book a time slot. In which parts of the flow would you prefer strong consistency, and where would eventual consistency be acceptable? Explain the trade-offs in terms of correctness, user experience, and scalability.
Sample Answer
I would use strong consistency where correctness has immediate user impact, and eventual consistency where the system can tolerate brief lag.
Strong consistency is best for
- Creating a hold on inventory
- Confirming or cancelling a reservation
- Charging a payment or finalizing a booking state
These are write paths where two users cannot both succeed for the same slot. If the system is inconsistent here, double-booking or double-charging can happen.
Eventual consistency is acceptable for
- Search indexes
- Listing pages and cached availability summaries
- Notifications, analytics, and recommendations
A short delay in these areas usually affects freshness, not correctness. That is an acceptable trade-off because it improves scalability and lowers latency.
Trade-off
- Strong consistency gives safer correctness, but usually costs more in latency and coordination.
- Eventual consistency scales better and keeps the product fast, but the UI must tolerate stale reads.
For a marketplace, I would keep the booking commit path strict and transactional, while letting discovery and reporting flow through asynchronously updated systems.
Describe a measurement-driven method to set latency budgets and SLOs for a backend API used by a mobile app. Outline steps to collect baseline metrics, segment users by region/device, map SLOs to business metrics (e.g., conversion), choose percentile targets (p50/p95/p99), and set error budgets and escalation policies.
Sample Answer
Approach (overview)
I’d take a measurement-driven loop: collect real metrics, analyze segments, map latency to business impact, pick SLO percentiles, and define error budgets + escalation.
1) Baseline metrics collection
- Instrument API with distributed tracing (OpenTelemetry), and metric counters/gauges (Prometheus).
- Capture request latency, status codes, user-agent, region, user-id hash, endpoint, and payload size.
- Collect at least 2–4 weeks of traffic to cover weekly patterns.
2) Segment by region/device
- Slice latency by region, carrier, OS (iOS/Android), and device class (phone/tablet) using tags.
- Produce heatmaps and percentiles per segment to spot outliers (e.g., mobile carriers > 200ms).
3) Map SLOs to business metrics
- Run experiments: correlate latency buckets with conversion/dropoff and revenue per session.
- Example: p95 > 500ms increases checkout abandonment by 8%. Use that to set stricter SLOs for checkout endpoints.
4) Choose percentile targets
- Use p50 for user experience trends, p95 for tail latency affecting many users, p99 for critical worst-case.
- Example: global API SLO = p95 < 300ms, critical checkout endpoint = p99 < 500ms.
5) Error budget & escalation policy
- Define error budget = 1 - availability/SLO over a rolling 30-day window (e.g., 99% SLO → 1% budget).
- Policies:
- If budget burn rate > 2x in 24h → Pager to on-call, pause non-essential launches.
- If budget used > 50% in a week → freeze feature releases, schedule remediation sprint.
- If budget exhausted → rollback risky changes, postmortem and SLA communication.
6) Iterate
- Monitor, run A/B tests for latency optimizations (caching, compression, dial timeouts), and revise SLOs as business impact data evolves.
This ties backend metrics to user outcomes and gives clear operational actions for a backend team.
Compare role-based access control (RBAC), attribute-based access control (ABAC), and policy-based access control (PBAC). Explain core concepts, provide one concrete example where each excels (enterprise admin vs dynamic resource policy), discuss advantages and disadvantages, and list considerations and pitfalls when migrating a large organization from RBAC to ABAC/PBAC.
Sample Answer
Direct answer
Role-based access control (RBAC) assigns permissions to named roles and roles to users, so "can this user do X" reduces to a role lookup. Attribute-based access control (ABAC) evaluates a policy against attributes of the user, the resource, and the environment at request time, so the decision is computed dynamically rather than pre-wired into a role assignment. Policy-based access control (PBAC) externalizes that decision logic into declarative, centrally-managed policies evaluated by a dedicated policy engine; it is best understood as an architectural evolution of how RBAC- or ABAC-style rules get authored and evaluated, not as a fourth independent permission model sitting beside the other two.
Structured elaboration
| Model | Core concept | Granularity | Where it excels |
|---|---|---|---|
| RBAC | Permissions attached to a small set of named roles, roles assigned to users | Coarse, role-level | An enterprise admin tool with a stable set of job functions (Admin, Editor, Viewer) where "what can an Editor do" rarely changes |
| ABAC | A policy evaluated against attributes of the subject, resource, action, and environment at request time | Fine, per-request | A dynamic resource policy where access legitimately depends on context, for example allow if the user's department matches the resource's department, the request falls within business hours, and the user's clearance level is at least the resource's sensitivity level |
| PBAC | Declarative policies, which can encode role rules, attribute rules, or a mix, evaluated by a centralized policy engine decoupled from application code | Whatever the policy language expresses | An organization that needs one auditable, centrally-versioned source of truth for access decisions across dozens of services, instead of each service embedding its own ad hoc role-check logic |
Advantages and disadvantages of each:
- RBAC is simple to reason about and easy to audit (answering "who has the Admin role" is one query), and it maps naturally onto real job functions. Its failure mode at scale is role explosion: a role per department times seniority times project combination, and coarse granularity that can't express something as simple as "only during business hours" without inventing yet another role.
- ABAC is expressive and avoids role explosion entirely, since context-dependent rules are policy, not new roles. Its cost is that auditing becomes harder ("what can this user do" now requires evaluating the policy against every resource shape rather than a single lookup), and it introduces a new dependency: the attributes must come from somewhere and stay fresh, or the decision is silently wrong.
- PBAC centralizes and versions the actual decision logic and can unify RBAC-shaped and ABAC-shaped rules under one evaluation point, generally improving consistency across services. The cost is operational: the policy engine becomes a critical-path dependency (whether run centrally or as a per-service sidecar, each has its own latency and availability profile), and policy authoring is a skill investment most application teams don't already have.
Worked example
Consider a 200-engineer company that started with a handful of RBAC roles (admin, engineer, contractor) and, after three years of ad hoc requests, has accumulated 60 near-duplicate roles like engineer-emea, engineer-emea-contractor, senior-engineer-payments-team, one for nearly every department, region, and project combination. That is role explosion in practice, and it's the concrete trigger for migrating.
A realistic migration path:
- Audit existing role assignments to reverse-engineer the attributes actually driving each role's boundary.
engineer-emea-contractoris really encoding three separate attributes:department=engineering,region=emea,employment_type=contractor. Extract those explicitly rather than guessing at a new policy from scratch. - Define the attribute schema and its sources of truth: department and employment type from the HR system, region from the identity provider's user record, clearance level from a separate compliance system. This is the step teams most often underestimate.
- Run the new policy in shadow mode: evaluate the ABAC or PBAC decision alongside the existing RBAC check on every request, without enforcing it, and diff the two outcomes. Only cut over once the diff rate is acceptably close to zero and every remaining mismatch is explained.
- Keep RBAC where it still fits: the company's internal admin console, with its handful of stable job functions, has no reason to become attribute-driven just because the resource-access layer did. A PBAC-style policy engine can enforce both the coarse RBAC rule for the admin console and the fine-grained ABAC rule for resource access, which is exactly the "policies can encode either shape" property that distinguishes PBAC from being a third standalone model.
Trade-offs and pitfalls
- The attribute source-of-truth problem is the largest real operational-complexity cost of moving off RBAC: if department, clearance, and region live in three different systems and any one goes stale, the authorization decision is silently wrong in a way that is far harder to notice than an obviously misconfigured role. Budget for attribute governance as its own workstream, not a footnote to policy authoring.
- Policy testing does not scale linearly with the number of attributes; it scales combinatorially, since the interesting bugs live in attribute combinations. Build an automated policy test suite before cutover, not after, or the shadow-mode diff in step 3 above will be too noisy to interpret.
- Explainability is easy to lose. A role name is self-documenting: "why can they do this? they're an Editor." A dense attribute policy can become just as opaque as the role sprawl it replaced if it isn't kept small, well-commented, and reviewed like the security-critical code it is.
- Resist migrating everything to ABAC or PBAC just because it is available. Coarse, stable, rarely-changing access (internal admin tooling, a handful of job functions) is exactly what RBAC was built for, and adding attribute-based complexity there buys nothing but harder audits.
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 Backend Developer jobs
AI-enriched listings across hundreds of company career pages
Explore Jobs