Everything They AskInterview
0%

Map  ·  every topic a technical interview can reach

Everything
they ask.

Not a question bank. A map of the subject — thirteen areas, forty-six topics, and under each one the full list of things that can come up, each with the mechanism, the field names and the failure mode rather than a definition. The point is coverage first: you should be able to read this page and find the words you do not know yet, then find enough under each to know whether you could defend it. Where a topic has a page of its own elsewhere in this series, it says so and links there rather than repeating it.

5–7 roundsA typical senior loopA screen, one or two coding rounds, system design, a low-level design or deep dive, and a hiring-manager conversation. Every one is written up against the same rubric.
~35 minOf a 45-minute roundIntros and your questions take the rest. A perfect answer at minute 44 scores below a good one explained at minute 30, because the interviewer has to write down what they saw.
4 signalsWhat is actually scoredProblem solving, technical depth, code or design quality, and communication. Everything below is a way of producing evidence for one of those four.
Part one

The loop, and how it is scored

Before any subject: the process. A technical interview is not one conversation, it is four or five of them written up separately by people who then argue in a room you are not in. Knowing what each round is for, and what the writing-up looks like, changes how you spend the forty-five minutes more than any amount of extra revision.

Topic · 01

What the rounds are and what each one is for

Every round has a question it exists to answer. Answer a different one well and you still fail the round — the classic being a system-design session spent writing code.

ONE LOOP, STAGE BY STAGE — THE QUESTION EACH ONE ANSWERS recruiter screen Is the level, the team and the money worth both sides’ time? 20–30 min technical screen Can this person write correct code at all? A filter, not a ranking. 45–60 min coding I Given a problem with a known shape, do they find the shape? 45 min coding II The same again, harder, or with an ugly requirement bolted on. 45 min system design Given no requirements, do they produce some? Ambiguity is the test. 45–60 min deep dive or LLD Objects and interfaces, or your own past work under questioning. 45 min hiring manager Scope, collaboration, conflict, judgement — and would I hire them? 30–45 min debrief Written up independently, then argued. You are not in the room. you: absent Each interviewer sees one round. Nothing you established earlier carries over, so every round starts from zero — say the important things again. Smaller companies compress this into two or three sessions; the questions being answered do not change, only how many people ask them.
The debrief is the round you cannot attend, so write its notes for it. An interviewer has to reconstruct forty-five minutes from memory a few hours later. Whatever you said clearly, once, near the end — the complexity, the trade-off, the thing you would do differently with more time — is what survives into the write-up. Everything you thought silently does not exist.

Round names vary; the questions do not. A “pair programming” round is a coding round with a live codebase, a “practical” is a coding round with a debugger, an “architecture” round is system design, and a “values” or “bar raiser” round is a behavioural round run by someone outside the team who has a veto. Ask the recruiter which is which before the loop — that request is normal and the answer is usually detailed.

Technical screenOne problem in a shared editor, 45 to 60 minutes, often with no autocomplete and no run button. It exists to filter, not to rank, so it is graded generously on style and harshly on whether the thing worked. Two practical consequences: rehearse in a plain text box at least once, and finish something that runs rather than starting something elegant.
Coding roundData structures and algorithms, spoken aloud, with the interviewer typing notes while you type code. The narration is scored as heavily as the solution — a correct answer produced in silence is a weak write-up, because the interviewer has nothing to quote. Expect one problem with a follow-up that changes a constraint, and expect to be asked for complexity before you are asked for anything else.
System designFrom senior upward, occasionally at mid level. You get two or three sentences and no requirements, because producing the requirements is the exercise. There is no right answer and there are many wrong ones; the grade comes from whether each box on your diagram was placed to relieve a named pressure, and whether you said what it cost.
Low-level designCommon in India, at product companies, and anywhere the team writes a lot of domain code. Model a domain in classes and interfaces — a parking lot, an elevator bank, a rate limiter — usually on a whiteboard or in an editor with nothing running. Graded on boundaries, naming and extensibility, and almost always with one concurrency question bolted on at the end.
Deep diveYour own project, interrogated. The pattern is “why that database?” three times in a row, until either the reasoning bottoms out in something real or the story runs out. Pick a project where you made the decisions, know the numbers, and can name what you would do differently — and never claim a technology you cannot defend two questions deep.
Debugging or pairingA repository you have never seen and a failing test or a broken endpoint. Method is the whole grade: reproduce, read the error properly, form a hypothesis out loud, bisect, change one thing. Finding the bug quickly with no visible method scores worse than finding it slowly with one, because only the method generalises to the job.
Take-homeTwo to four hours of real code, then a review conversation. The conversation is the round; the code is only the prompt for it. Reviewers look for a README saying how to run it and what you traded away, a commit history that shows the order you worked in, tests that would fail if the code were wrong, and something sensible on the unhappy path.
Domain roundFrontend, mobile, data, machine learning, infrastructure or security specifics — the round that is different for each track and the one candidates prepare least. See part eleven for what each one actually contains. A strong generalist who skips this is exactly the profile that fails a specialist loop.
BehaviouralPast behaviour taken as evidence of future behaviour, asked from a script, scored on a rubric, and far less of a formality than it looks. At senior level it carries similar weight to a coding round, because “what is this person like when something goes wrong” is the question the team actually cares about. Five prepared stories cover nearly every prompt.
Hiring managerFit, scope, and the two-way sale. It is the only round where their answers matter as much as yours: this is where you find out what the team shipped last quarter, how on-call works, and whether the role is the one advertised. Vague answers to concrete questions here are the most reliable warning sign in the whole process.
  • Walk me through your last project.whether you can pick the right altitude — the problem, the constraint, your decision, the outcome — without narrating the sprint board.
  • What are you looking for in your next role?whether the honest answer is compatible with the job they have open. It is a matching question, not a trap.
  • How long do you need for this problem?calibration. Interviewers ask it to see whether you can size work, including your own.
Topic · 02

The rubric: four signals, and what each looks like at each level

Almost every company scores the same four things, and the level you are hired at is decided by which cell of this grid your evidence lands in — not by how many questions you finished.

WHAT THE WRITE-UP HAS TO SUPPORT JUNIOR MID SENIOR STAFF problem solving Reaches a working answer once nudged. Gets there unaided; takes the obvious speed-up. Picks the trade before coding; says its cost. Reframes so the hard part disappears. technical depth Knows the vocabulary and the hazard. Knows how the tool behaves under load. Knows why, and names its failure modes. Knows where the abstraction leaks. code and design Compiles; names read like the problem. Handles edge cases and tests the awkward one. Boundaries in the right place; errors are typed. Others can extend it without asking you. communi- cation Answers the question that was asked. Thinks out loud; takes a hint without stalling. Drives the session and states assumptions. Makes the trade-off legible to the room. A cell you cannot supply evidence for is not scored as average — it is scored as no data, which reads as a miss.
The columns are cumulative. Nobody skips a column: a staff-level write-up still has to contain the junior-level evidence, so the fastest way to fail a senior loop is to be interesting about trade-offs while the code does not compile. Get the working answer first, then reach right along the row.

Two mechanics decide what happens to those scores. The first is that interviewers write up independently before seeing each other’s notes, precisely so that one strong opinion cannot anchor the rest — which is why repeating your best point in every round is not repetition, it is coverage. The second is that most rubrics have a veto somewhere: a bar-raiser, a hiring committee, or a manager who can decline a candidate everyone else liked. A single strong no hire usually outweighs three weak yeses.

Level is decided by evidence, not by the job title on your CV

“Downlevelling” almost always traces to one thing: the stories were about tasks rather than decisions. Two people can do the same work for four years and only one of them can say what they chose, what they gave up and how they knew it worked. Part twelve is about building that vocabulary from work you have already done.

Topic · 03

How to spend forty-five minutes

The same clock applies to a coding round, a design round and a debugging round. Candidates who fail on time nearly always fail it in the first ten minutes, by starting before the problem is agreed.

THE SAME FORTY-FIVE MINUTES, SPENT TWO WAYS strong 1 clarify 2 agree 3 build it, narrating 4 run it 5 wrap struggling silent coding, then a rewrite at minute 26 still typing when time is called 0 10 min 22 min 33 min 45 min 1  CLARIFY  ·  0–5 Inputs, sizes, ranges, duplicates, empty, what “best” means. Write the examples down. 2  AGREE  ·  5–10 State the approach and its complexity, then ask “shall I write that?” before typing a line. 3  BUILD  ·  10–32 Write it while saying what each part does. Silence for more than a minute is a lost signal. 4  RUN IT  ·  32–39 Trace your own worst example by hand, out loud. Find your bug before they point at it. 5  WRAP  ·  39–45 Complexity in both dimensions, what you would change with more time, then your questions. Phases 1, 2 and 5 are the cheapest marks on the page and the ones most often skipped entirely.
Phase two is a contract. “A hash map of counts, one pass, linear time and linear space — shall I write that?” costs fifteen seconds and buys two things: if the interviewer wanted something else you find out now rather than at minute thirty, and if you run out of time the write-up records that you had the right approach. Both of those are worth more than the last ten lines of code.
  1. Repeat the problem back in one sentence. Half of all misunderstandings die here, and it costs ten seconds.
  2. Ask about scale before you ask about anything else. Ten elements and ten billion elements are different problems, and the interviewer is usually waiting to be asked.
  3. Say the brute force out loud, then discard it. It proves you understand the problem and it establishes the baseline the improvement is measured against. Never write it unless asked.
  4. Take the hint. A hint is not a penalty; ignoring one is. “Have you thought about sorting first?” means sort first.
  5. When stuck, say what you know. “I know it is a graph problem and I know BFS gives shortest edges, but I cannot see what the nodes are” is a scoreable sentence. Staring is not.
  6. Never claim something you cannot defend. One unbacked “we used Kafka for exactly-once delivery” can cost more than an unfinished function, because it makes every other claim suspect.

The remote round has its own failure modes

Test the shared editor beforehand — several have no autocomplete, no imports and no run button. Keep a second device on the call as a backup, use a headset so typing does not drown you out, and share your screen rather than reading code aloud from a window they cannot see. If the connection drops, rejoin and carry on; interviewers extend for it and never score it.

Part two

The coding round

One or two rounds, forty-five minutes each, a problem with a known shape and an interviewer watching you find it. The subject is finite: a few dozen structures, a few dozen algorithms, and about fifteen patterns that connect a problem statement to one of them. This part is the full list, plus the two things that decide the score once the code works — complexity you can defend and correctness you demonstrated yourself.

Topic · 04

Every structure and every algorithm, in one list

Coverage first. If a word below is unfamiliar, that is the gap — not the twelfth variation of a problem you can already solve. The bolder terms are the ones that appear in a majority of loops.

Data structures are asked in three ways: build one, use one to make something faster, or explain what the standard library’s version costs. You need the operations, the complexity of each, and the memory behaviour — a hash map that rehashes and an array that reallocates are the same fact told twice.

Linear and associative

arraydynamic arrayamortised growthstringstring builderimmutable stringssingly linked listdoubly linked listcircular listsentinel nodestackqueuedequecircular buffermonotonic stackmonotonic dequehash maphash sethash functionseparate chainingopen addressinglinear probingload factorrehashingordered mapmultisetbitsetsparse arrayropepersistent structure

Trees, heaps and search structures

binary treebinary search treetree traversalinorder / preorder / postorderlevel orderMorris traversalbalanced treeAVL rotationred–black treetreapsplay treeB-treeB+ treeskip listbinary heapd-ary heappriority queueheapifyindexed heaptwo-heap mediantrieradix / Patricia treesuffix triesuffix arraysegment treelazy propagationFenwick / BITsparse tableinterval treek-d treequadtreeR-tree

Graphs, sets and probabilistic structures

adjacency listadjacency matrixedge listimplicit graphweighted graphdirected acyclic graphdisjoint set unionunion by rankpath compressionLRU cacheLFU cachebloom filtercount–min sketchHyperLogLogconsistent hashing ringmerkle treeskip graph

Algorithms divide into families, and the families are what you memorise — not the problems. A candidate who knows six families and can recognise them beats one who has solved four hundred problems by pattern-matching titles.

Scanning, searching and sorting

two pointersfast and slow pointerscycle detection (Floyd)sliding windowvariable windowprefix sumsdifference array2-D prefix sumsbinary searchbinary search on the answerlower / upper boundternary searchmerge sortquicksortpartition schemesDutch national flagheap sortinsertion sortcounting sortradix sortbucket sortTimsort / introsortstabilitycustom comparatorquickselectcyclic sortmerge k sortedinterval mergesweep linemeet in the middle

Recursion, greedy and dynamic programming

recursionrecursion treetail callbacktrackingpruningsubsetspermutationscombinationsN-queensSudoku solverdivide and conquermemoisationtabulationstate definitiontransitionbase caserolling array0/1 knapsackunbounded knapsackcoin changelongest increasing subsequencelongest common subsequenceedit distancepalindrome partitioningmatrix chaininterval DPtree DPdigit DPbitmask DPDP on subsetsKadanegreedyexchange argumentactivity selectionHuffman codingfractional knapsackjob scheduling

Graph algorithms

breadth-first searchdepth-first searchiterative DFSmulti-source BFSbidirectional BFS0–1 BFStopological sortKahn’s algorithmcycle detectioncolouringbipartite checkconnected componentsflood fillTarjan SCCKosarajubridgesarticulation pointsDijkstraBellman–Fordnegative cyclesFloyd–WarshallA* searchJohnsonminimum spanning treePrimKruskalEuler tourHamiltonian pathlowest common ancestorbinary liftingheavy–light decompositionmax flowFord–FulkersonEdmonds–KarpDinicmin cutbipartite matchingHungarian algorithm

Strings, maths and the rest

string matchingrolling hashRabin–KarpKMP failure functionZ-algorithmManacherAho–Corasicksuffix automatonanagram groupingtokenisingexpression evaluationshunting yardbit manipulationpopcountXOR trickslowest set bitsubset enumerationGCD / Euclidextended Euclidmodular arithmeticmodular inversefast exponentiationsieve of Eratosthenesprime factorisationcombinatoricsCatalan numbersinclusion–exclusionmatrix exponentiationprobabilityexpected valuereservoir samplingFisher–Yates shufflerandomised pivotconvex hullline intersectionclosest pairoverflowfloating pointdesign a data structureiterator designconcurrency puzzle

These have a page of their own

Every structure and algorithm above is worked through, with figures you can step, in Data Structures, In Motion — the eighth page in this series, linked in the rail. This page’s job is the checklist; that page’s job is the mechanism.

Topic · 05

The fifteen patterns that connect a problem to a structure

Interview problems are disguised. The disguise is the story; the cue is in the constraints. Learning to read the cue is most of what “being good at these” means.

WHAT THE PROBLEM SAYS → WHAT TO REACH FOR sorted, or worth sorting two pointers, binary search contiguous subarray sliding window, prefix sums top k, or a stream heap of size k, quickselect next greater or smaller monotonic stack counts, seen before, dedupe hash map, hash set prefixes, autocomplete trie grid, maze, islands BFS or DFS, implicit graph fewest steps, shortest path BFS; Dijkstra if weighted dependencies, ordering topological sort grouping, merging, connectivity disjoint set union overlapping ranges sort by start, sweep line every arrangement, every board backtracking, pruned count the ways, best score dynamic programming smallest value that works binary search the answer range query plus updates Fenwick or segment tree pattern inside text rolling hash, KMP n is at most about 20 bitmask over subsets no extra space allowed in-place swaps, cyclic sort The cue is in the constraints, not the story. Read the bound on n and the memory limit first; between them they usually name the algorithm.
Read the bound on n as a complexity budget. n ≤ 20 means exponential is expected, so stop looking for the clever polynomial. n ≤ 2000 admits O(n²), which is usually a two-dimensional table. n up to a million rules out anything worse than O(n log n), which leaves sorting, a heap, or one pass with a hash map. This single reading eliminates most wrong turns before you have taken one.

Two patterns are worth writing out because candidates who know them by name still get the boundaries wrong under pressure. The first is binary search on the answer: when the question asks for the smallest capacity, the minimum speed or the largest minimum, the search space is not the array — it is the range of possible answers, and the array only appears inside a feasibility check that is monotone.

# feasible.py — binary search over the ANSWER, not over the input.
# Cue: "minimum capacity", "smallest k such that ...", "maximum minimum".
def min_capacity(weights: list[int], days: int) -> int:
    def fits(cap: int) -> bool:
        # Greedy is safe here only because fits() is monotone in cap:
        # if a capacity works, every larger capacity works too.
        used, load = 1, 0
        for w in weights:
            if load + w > cap:
                used, load = used + 1, 0
            load += w
        return used <= days

    lo, hi = max(weights), sum(weights)   # lo must itself be feasible-or-not
    while lo < hi:                        # invariant: answer is in [lo, hi]
        mid = lo + (hi - lo) // 2         # never (lo+hi)//2 in a fixed-width int
        if fits(mid):
            hi = mid                      # mid may be the answer — keep it
        else:
            lo = mid + 1                  # mid is definitely not — discard it
    return lo                             # lo == hi, and it is feasible

The second is the monotonic stack, which answers “for each element, where is the next one bigger than it?” in one pass. The trick to state out loud is that each index is pushed once and popped once, so the nested-looking loop is linear, not quadratic — that sentence is the whole complexity argument and interviewers wait for it.

# next_greater.py — one pass, each index pushed once and popped once: O(n).
def next_greater(nums: list[int]) -> list[int]:
    out = [-1] * len(nums)
    stack: list[int] = []              # indices, values strictly decreasing
    for i, v in enumerate(nums):
        # Everything smaller than v has just found its answer.
        while stack and nums[stack[-1]] < v:
            out[stack.pop()] = v
        stack.append(i)
    return out                         # whatever is left never found one: -1
Two pointersTwo indices moving under a rule that makes the discarded half provably useless. On a sorted array from both ends: if the sum is too small only the left pointer can help, so moving it discards nothing that could have been an answer — that sentence is the correctness proof and interviewers wait for it. Same direction gives you the window below. Linear time, constant space, and it dies the moment the array is not sorted and cannot be.
Sliding windowA contiguous range with an invariant you restore by shrinking from the left. Fixed size is arithmetic: add the new element, drop the old one. Variable size needs a shrink condition — “while the window has a repeat, advance left” — and each index enters and leaves once, so it is O(n) even though it looks nested. Say which invariant the window maintains and the code writes itself.
Fast and slow pointersOne pointer moves twice as fast. If there is a cycle they must meet inside it, and if you then restart one from the head at equal speed they meet at the entry — the arithmetic behind that is worth being able to sketch. Also gives you the middle of a list in one pass and the k-th from the end without a length. Constant extra space, which is usually the point of the question.
Prefix sumsPrecompute running totals once, then any range sum is a subtraction: O(n) setup, O(1) per query. The upgrade that turns it into a real interview pattern is a hash map of prefix values seen so far, which converts “count subarrays summing to k” from quadratic to linear. Two dimensions works the same way with inclusion–exclusion, and a difference array is the inverse trick for range updates.
Binary searchOn a sorted array, on a rotated one, or on the range of possible answers. Fix the invariant first — “everything left of lo is too small, everything from hi on is still a candidate” — and it determines whether the loop is < or <= and whether you assign mid or mid + 1. Guessing at those two is the single most common way a strong candidate loses ten minutes.
Binary search on the answerWhen the question asks for the smallest capacity, the minimum speed or the largest minimum, the search space is not the input — it is the range of answers, and the input appears only inside a feasibility check. It works exactly when that check is monotone: if a value works, every larger one does. Say the monotonicity out loud, because it is the whole justification.
Heap and top-kA min-heap of size k for the k largest: push, and pop when the size exceeds k, so the root is the k-th largest at all times. O(n log k), which beats sorting when k is small and works on a stream where sorting cannot. Quickselect beats both at O(n) expected when you are allowed to reorder the array and only need one query. Two heaps facing each other give you a running median.
Monotonic stackA stack whose values stay sorted, so pushing a new element pops everything it dominates — and each popped index has just found its answer. Next greater element, daily temperatures, stock span, largest rectangle in a histogram, trapping rain water. Each index is pushed once and popped once, so the nested-looking loop is linear; that sentence is the complexity argument.
Interval sweepSort by start, then walk once. Merging is “does this one start before the last one ended”; counting overlaps is a sweep over start and end events with a running counter, which is the meeting-rooms answer. Sorting dominates at O(n log n). Ask whether touching intervals count as overlapping before you write the comparison — it is the intended trap.
BFS and DFSGrids, boards and state machines are graphs whose edges are implied, and seeing that is most of the difficulty. BFS with a queue gives fewest edges, which is shortest path only when every edge costs the same; DFS with recursion or an explicit stack gives reachability, components and structure. Mark visited when you enqueue, not when you dequeue, or the queue fills with duplicates.
Topological sortAnything phrased as prerequisites, build order or dependencies. Kahn’s algorithm repeatedly takes a node of in-degree zero; if it finishes having emitted fewer nodes than exist, the remainder is a cycle — so you get cycle detection for free, which is almost always part two of the question. The DFS variant gives the same order reversed, and detects the cycle with a three-colour marking.
Disjoint set unionGrouping under merges: two operations, find and union. With union by rank and path compression together, each operation is effectively constant — say “inverse Ackermann” once and move on. It answers connectivity questions, powers Kruskal, and detects a cycle while adding edges. What it cannot do is un-merge, so if the problem removes edges, it is the wrong structure.
BacktrackingChoose, recurse, un-choose. The enumeration is mechanical; the score is in the pruning — sort first so you can break early, skip duplicates by comparing with the previous candidate, and bound the partial solution against the best so far. State the branching factor and the depth and the complexity follows, which is usually O(kn) or O(n!) and is expected to be.
Dynamic programmingSay four things before writing a line: what the state means in words, the transition from smaller states, the base case, and the order that guarantees dependencies are ready. Memoised recursion and a bottom-up table are the same recurrence written twice; recursion is easier to derive, tabulation is easier to make O(1) in space with a rolling array. If you cannot say the state in one English sentence, the recurrence will be wrong.
GreedyTake the locally best choice and never revisit it. Only correct with an argument: an exchange argument shows that any optimal solution can be transformed into yours without getting worse. If you cannot construct one in thirty seconds, look for the counterexample instead — and either result is a good answer. Greedy with no justification is the most common way to be confidently wrong in a coding round.
Bitmask over subsetsWhen n is at most about twenty, a set of elements is an integer and a set of sets is an array indexed by that integer. 1 << n states, iterate submasks with m = (m - 1) & mask, count bits with a popcount intrinsic. Assignment problems, travelling salesman on small n, and covering problems. The bound on n in the statement is what tells you this is the intended solution.
Topic · 06

Complexity, in the way it is actually asked

Not “what is big-O”. The real questions are: which class does this fall into, does the bound hold in the worst case as well as on average, what does it cost in space, and would that fit in a second.

HOW BIG CAN n BE AND STILL FINISH IN ABOUT A SECOND COMPLEXITY LARGEST n ONE MARK PER FACTOR OF TEN O(log n) anything O(sqrt n) 10^16 O(n) 100 million O(n log n) 5 million O(n sqrt n) 200 000 O(n^2) 10 000 O(n^3) 460 O(2^n) 26 O(n * 2^n) 22 O(n!) 11 Assumes roughly 10⁹ simple operations per second — near enough for a whiteboard, an order of magnitude out for a slow language or cache-hostile access.
Run the table backwards. The bound on n in the problem statement tells you which row you are allowed to land in, and therefore which family of algorithm the setter had in mind. It also tells you when to stop optimising: if n is 2000 and O(n²) fits, spending fifteen minutes hunting for O(n log n) is a worse use of the round than finishing and testing the quadratic one.
What they askWhat a weak answer sounds likeWhat lands
Time complexity“O(n) I think”“One pass over n, and each step is a hash lookup which is O(1) amortised — so O(n), and O(n²) if the hash degenerates.”
Space complexitySilence, or “O(1)” while holding a mapThe output, the auxiliary structures and the recursion stack, counted separately.
Worst vs averageQuoting the average as if it were the bound“Quicksort is O(n log n) expected, O(n²) worst; a random pivot makes the worst case unlikely rather than impossible.”
Amortised“Appending is O(1)”“O(1) amortised: doubling makes the occasional copy pay for the cheap appends before it. A single append can still be O(n).”
RecurrenceGuessingT(n) = 2T(n/2) + O(n) → O(n log n), by the master theorem or by drawing the tree.
Why it is slow in practice“Big-O says it is fine”Constant factors, cache locality, allocation, and the difference between a linked list and an array with the same bound.

Also fair game

big-O, big-Ω, big-Θtight vs loose boundamortised analysisaggregate methodpotential methodmaster theoremrecursion treesubstitution methodspace–time trade-offin-placestable sortonline vs offline algorithmexpected timeLas Vegas vs Monte CarloNP-completenessreductionapproximation ratiocache localityconstant factors
Topic · 07

Correctness: invariants, edge cases, and testing your own code

The difference between a hire and a no-hire is often not the algorithm — it is who found the bug. Finding your own scores; having it found for you costs.

An invariant is a sentence that is true before and after every iteration. Say it out loud and most boundary bugs become impossible to write: “everything left of lo is too small, everything from hi on is a candidate” determines whether the loop is < or <= and whether you assign mid or mid + 1. Guessing at those two decisions is the single most common source of a failed binary search under time pressure.

Empty and oneEmpty input, one element, two elements. Roughly a third of interview bugs live in these three, because loops that assume a “previous” element and pointers that assume room to move both break here and nowhere else. Test them out loud before the interviewer asks.
All elements equalKills naive quicksort partitioning into O(n²), breaks two-pointer conditions that assume strict inequality, and turns “find the second largest” into a definition question. Ask whether “second largest” means second distinct.
Already sorted, reverse sortedThe worst case for a fixed pivot and the best case that hides a bug — code that only ever ran on sorted input passes its own tests and fails in review. If you sort as a first step, say whether you are allowed to mutate the caller’s array.
DuplicatesAsk explicitly, because they change the answer rather than the code. Duplicates decide whether a set is a valid substitute for a map, whether binary search should find the first or the last match, and whether the output needs deduplicating. In backtracking they are the entire difficulty.
Negatives and zeroSign flips break greedy arguments, prefix-sum tricks, and anything reasoning about products. Zero is its own case in division, in modular arithmetic, and in “maximum product subarray”, where it resets the running state. Ask for the range of the input rather than assuming it is positive.
Integer overflow(lo + hi) / 2 overflows a 32-bit int at large indices; use lo + (hi - lo) / 2. A sum of a million values exceeds 32 bits, and a product exceeds 64 bits far sooner than people expect. Say it even in Python, where integers are arbitrary precision — naming a hazard your language happens to remove is a strong signal, not a wasted sentence.
Not found, and the empty answerWhat does the function return when nothing matches — a sentinel, null, an exception, an empty list? Decide before writing the loop, because the answer determines the return type and half the branches. A function that returns -1 and a function that throws are different contracts, and mixing them is what makes a caller wrong.
Off-by-one and the half-open rangePick one convention for the whole function and say which. Half-open [lo, hi) makes the length hi - lo, makes the empty range representable, and makes splitting a range trivial — which is why every standard library uses it. Inclusive bounds are fine too, as long as you never mix them within one function.
Cycles and self-loopsIn linked lists, in graphs, and in anything built from user input. A traversal with no visited set on a cyclic input does not return a wrong answer; it does not return. If the input is a tree “by construction”, ask who guarantees that.
Unicode, case and normalisation“Reverse a string” and “is it a palindrome” are different problems once a code point is not a character: combining marks, surrogate pairs, and a grapheme cluster made of several code points. Case folding is locale-dependent. Ask whether the input is ASCII — usually it is, and asking still scores.
Mutating the inputAre you allowed to sort in place, reuse the array as scratch, or write into the argument? Some questions are entirely about this: “do it with O(1) extra space” means the input is your workspace. Ask, because guessing wrong invalidates an otherwise correct solution.
Recursion depthA linked list of a million nodes overflows a recursive solution long before it runs out of time, and most languages give you a few thousand frames rather than a few million. Mention it, then offer the iterative form or an explicit stack — the offer is the signal, not the conversion.
Concurrency, if it is mentioned at allIf the problem says “many threads” even once, the interesting part is no longer the algorithm. Say what is shared, what the critical section is, and whether the operation can be made a single atomic step instead of a locked one.

Then run it. Not “it looks right” — take a small awkward input, put a finger on the first line, and say what every variable holds after each step. Two minutes of this catches more than ten minutes of re-reading, and it is visible work: the interviewer is writing down that you tested unprompted.

# two_sum.py — a dry run is a table, spoken. nums = [2, 1, 2], target = 4
#
#   i   v   need   seen                 action
#   0   2     2    {}                   miss -> seen[2] = 0
#   1   1     3    {2: 0}               miss -> seen[1] = 1
#   2   2     2    {2: 0, 1: 1}         hit  -> return (0, 2)
#
# Two things this catches that re-reading does not: that `seen` is written
# AFTER the lookup (otherwise an element pairs with itself), and that the
# duplicate 2 is fine because the first index is the one still stored.
def two_sum(nums: list[int], target: int) -> tuple[int, int] | None:
    seen: dict[int, int] = {}
    for i, v in enumerate(nums):
        if (need := target - v) in seen:
            return seen[need], i
        seen[v] = i
    return None
  • What is the complexity?both dimensions, and whether the bound you quote is worst case or average. Answer before you are asked and it counts for more.
  • Can you do it in less space?whether you know the standard trade: rolling arrays for dynamic programming, in-place partitioning, pointers instead of a copy.
  • What breaks if the input does not fit in memory?whether you can move from an algorithm to a system: external sort, streaming, sharding by key, a sketch instead of an exact count.
  • How would you test this?named categories rather than examples — empty, single, duplicates, sorted, adversarial — plus a property that must hold for any input.
Part three

The machine underneath

Hardware, the operating system, concurrency and the network. These four turn up in three disguises: as fundamentals questions in their own right, as the reason a system-design answer is wrong, and as the explanation for a bug in a debugging round. A candidate who can say why a thing is slow is scored a level above one who knows that it is.

Topic · 08

The processor, memory and the cost of a cache miss

Every performance question eventually reduces to one number: how far away the data is. Knowing the ladder by heart is the cheapest possible preparation and it pays out in coding rounds, design rounds and debugging rounds alike.

HOW FAR AWAY THE DATA IS — ONE MARK PER FACTOR OF TEN WHERE IT LIVES TYPICAL L1 cache 1 ns L2 cache 4 ns L3 cache 15 ns main memory 80 ns NVMe read 100 us same-DC hop 0.5 ms disk seek 8 ms London–New York 100 ms Scale one nanosecond up to one second and the ladder becomes human: L1 is a heartbeat, memory is a minute and a half, an SSD read is a day and a bit, a hop across the datacentre is a week, and the trip to New York takes three years.
The gap between memory and cache is why data layout beats cleverness. An array of structs walked in order costs one miss per cache line and the prefetcher hides most of that; the same data behind pointers costs a miss per element. This is the entire reason a linked list loses to an array with identical big-O, and it is the answer interviewers are hoping for when they ask why the “slower” algorithm won.
Cache line64 bytes, and it is the unit of transfer — reading one byte fetches all 64 into L1. That is why walking an array in order is nearly free (one miss per sixteen 4-byte elements, and the prefetcher hides most of even that) while chasing pointers costs a miss each. It is also why a struct’s field order changes its size, and why splitting a hot field out of a cold struct can double throughput without changing a line of logic.
LocalityTemporal — you will touch this again soon, so keep it. Spatial — you will touch its neighbour soon, so fetch the line. Almost every “make it faster” answer is one of these two wearing a costume: array of structs versus struct of arrays, blocking a matrix multiply into tiles, sorting before you scan, or simply making the working set fit in L2.
False sharingTwo cores writing two different variables that happen to share a cache line. Nothing is logically shared, but the coherence protocol invalidates the line on every write, so the two cores ping it back and forth and a parallel counter runs slower than a serial one. The fix is padding each to its own line — and the diagnosis is that scaling gets worse with more threads.
Branch predictionA modern core runs a hundred-odd instructions ahead of itself speculatively; a mispredicted branch throws that work away and costs roughly 15 cycles. The famous demonstration is that sorting an array first makes the identical filtering loop about twice as fast, because the branch becomes predictable. It is also the argument for branchless code — a conditional move or arithmetic instead of an if — when the condition is genuinely random.
Pipelining, superscalar, SIMDA core issues several instructions per cycle, out of order, across a deep pipeline, and a single instruction can operate on 8 or 16 lanes at once. So instruction count is a poor proxy for time: dependent operations serialise on latency while independent ones overlap, and the loop that vectorises beats the one that does not by an order of magnitude. This is why measuring beats reasoning below the algorithmic level.
Virtual memoryEvery address your program sees is virtual, translated per 4 KB page through a multi-level page table, with the recent translations cached in the TLB. A TLB miss costs a walk; a page fault costs a disk read. It explains the mystery slow first request after a deploy, why huge pages help a large heap, and why a fork is cheap until something writes.
NUMAOn a multi-socket machine, memory is attached to one socket and reaching the other one costs roughly twice as much. Turns up in database and JVM tuning as “why does pinning the process to one socket make it faster?” The answer is that the allocation and the thread ended up on different nodes.
Endianness and alignmentByte order matters the moment bytes go on a wire or into a file, which is why network formats specify big-endian and why you serialise explicitly rather than casting a struct. Alignment is why a struct of a bool, a 64-bit int and a bool is 24 bytes rather than 10 — and why reordering the fields makes it 16.
Word size, overflow and floating point32 versus 64 bit decides where your arithmetic wraps; signed overflow is undefined behaviour in C and C++, which is a correctness question and not a pedantic one. Floating point is binary, so 0.1 + 0.2 != 0.3, comparisons need a tolerance, and money is stored in integer minor units or a decimal type — never a double.
The cost model, in one ladderRegister, L1, L2, L3, memory, SSD, network, spinning disk — roughly 1, 1, 4, 15, 80 nanoseconds, then 100 microseconds, then milliseconds. Knowing those seven numbers turns most performance questions into arithmetic: how many of these do I do per request, and what does that multiply out to?

Also fair game

instruction setcycles per instructionclock speed vs IPCout-of-order executionspeculative executionSpectre / Meltdownwrite buffercache coherenceMESI protocolprefetchinghuge pagescopy-on-writeDMAinterruptcontext switch costhyper-threadingGPU vs CPUAmdahl’s lawLittle’s lawthe utilisation knee
Topic · 09

The operating system your service is a guest of

Processes, threads, scheduling, memory and files. Asked directly in systems-heavy loops, and indirectly everywhere else — “why did the container get killed?” is an operating-system question wearing a cloud costume.

Process versus threadA process owns an address space; threads inside it share one. Everything hard about concurrency follows from that word “share”, and everything expensive about processes follows from not sharing. A crash takes down one process; a corrupted heap takes down every thread. Isolation versus communication cost is the trade, and it is the same trade one level up between services and modules.
The schedulerRunnable threads sit in a queue and get a time slice; more runnable threads than cores means waiting, not more work done. This is why a machine at 100% CPU has latency that grows non-linearly rather than throughput that grows at all, and why load average counts runnable threads rather than measuring anything about the CPU.
Context switchA few microseconds of direct cost, plus the indirect cost nobody counts: the new thread arrives to a cold cache and a flushed pipeline. That indirect part is why a thread per request stops scaling long before the direct cost matters, and why event loops and green threads exist.
System callsThe boundary between your code and the kernel, costing a few hundred nanoseconds plus whatever it does. A syscall per byte is the bug; buffering is the fix, and it is why every language wraps a file in a buffered writer. strace (or dtruss) is the tool interviewers hope you name, because a syscall trace answers “what is it actually doing” when a profiler shows nothing.
The virtual address spaceText, data, heap growing up, stack growing down, and memory-mapped regions in between. A segmentation fault is a touch outside anything mapped; a stack overflow is recursion walking off its guard page. Knowing the layout is what makes a core dump readable and what makes “why is RSS bigger than my heap” answerable.
Paging and swapMinor faults map a page already in memory; major faults go to disk and cost a thousand times more. Thrashing is when the working set exceeds RAM and every access is a major fault, at which point the machine is alive and useless. The OOM killer ends the largest offender, which in a container is your process and shows up as exit code 137.
The page cacheThe kernel caches file contents in whatever RAM is spare, so a second read of the same file is free and “free memory” on a healthy machine is close to zero by design. It is why a database with 8 GB of buffer pool on a 64 GB box has far more cache than it admits, and why benchmark numbers collapse when you run them on a cold machine.
File descriptorsA small integer into a per-process table with a hard ceiling. Leak them — an unclosed response body, a connection never returned to a pool — and the symptom is EMFILE or “too many open files” under load and never in testing. lsof and /proc/<pid>/fd tell you which ones.
The four I/O modelsBlocking (one thread waits), non-blocking with polling (burns CPU), readiness notification with epoll or kqueue (the thread is told when a socket is ready, which is what every event loop is built on), and completion notification with io_uring or IOCP (the kernel does the work and tells you it is done). Naming readiness versus completion is what distinguishes a real answer from “async is faster”.
Signals and graceful shutdownSIGTERM asks, SIGKILL tells and cannot be caught. Every orchestrator sends the first, waits a grace period, then sends the second — so a service that ignores SIGTERM gets 30 seconds of grace and then loses whatever was in flight. Handling it means: stop accepting, finish what is running, close pools, exit.
Pipes, sockets and shared memoryThe three ways processes talk: a byte stream between relatives, a byte stream between anyone (including over a network), and a shared region with no copying and all the synchronisation problems. Most “how would you make these two components talk” questions are really asking which of these three, and why.
fsync and durabilityA successful write means the kernel has your bytes, not that the disk does. Only fsync forces the platter or the flash, and it costs milliseconds. That single fact is why write-ahead logs exist, why databases batch commits, why “group commit” is a tuning knob, and why a benchmark with fsync disabled is a benchmark of nothing.
Users and permissionsUID, GID, file modes, and the reason a container that runs as root gives an escaped process root on a namespace it may share more of than you think. Running as a non-root numeric user costs one line in a Dockerfile and removes a whole class of finding from every audit.
cgroups and namespacesWhat a container actually is: namespaces give it a private view of processes, mounts, network and users; cgroups give it limits. The two behave differently when exceeded — a memory limit kills, a CPU limit throttles — which is why an over-tight CPU limit shows up as latency and an over-tight memory limit shows up as a restart loop.
  • What happens between typing a command and the program running?fork, exec, the loader, dynamic linking, the entry point — and whether you can stop at the right depth for the role.
  • Your service is at 100% CPU but doing no work. What now?a method: is it user or system time, a spin loop, GC, a lock convoy? Naming top, perf and a flame graph is the expected answer.
  • Why was the container killed with exit code 137?128 + 9, so SIGKILL, so the memory cgroup limit — and the follow-up about heap versus RSS.
  • How does a thread pool of 200 threads behave differently from 20?context switching, memory per stack, and Little’s law: more threads do not create more capacity, they only lengthen the queue.
Topic · 10

Concurrency: races, locks and the memory model

The most reliably mishandled subject in the whole loop. Almost everyone can define a race condition; far fewer can say why a correctly locked program can still read stale data on another core.

TWO THREADS RUN counter++ ONCE EACH — THE COUNTER ENDS AT 1 thread A read x -> 0 add 1 -> 1 store 1 thread B read x -> 0 add 1 -> 1 store 1 x in memory 0 0 0 0 1 1 — not 2 mutex Only one thread in the section at a time. Costs a syscall when contended. atomic add One instruction the hardware makes indivisible. Fastest, but only for one word. compare-and-swap loop Read, compute, swap-if-unchanged, retry. The basis of every lock-free queue. The bug is not the increment. It is that read, modify and write are three steps and nothing says they happen together.
Then the second half of the question: visibility. Even with no interleaving, a write by one core may sit in a store buffer and never be seen by another until something forces it out. That “something” is a memory barrier, which is what a mutex release and an atomic store quietly emit. A variable read in a loop without one can spin forever on a value that changed minutes ago — the reason volatile, Atomic* and sync/atomic exist at all.
// race.go — go build -race is the only honest way to find these.
var counter int                       // shared, unprotected

func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        counter++                     // read, add, write: three steps
    }
}

// Three fixes, in increasing order of how much they let you do:

var mu sync.Mutex                     // 1. protects any amount of state
mu.Lock(); counter++; mu.Unlock()

var n atomic.Int64                    // 2. one word, no lock, no waiting
n.Add(1)

for {                                 // 3. read-modify-write on your own terms
    old := n.Load()
    if n.CompareAndSwap(old, old*2) { // fails if anyone else moved it
        break
    }
}
Race condition versus data raceA race condition is any bug whose outcome depends on timing — two requests both passing a balance check is one, even with perfect locking. A data race is the narrower, machine-level case: two threads touching the same memory with no synchronisation and at least one writing, which is undefined behaviour in most languages rather than merely wrong. Fixing the data race does not fix the race condition, and interviewers do ask for the distinction.
Critical sectionThe span that must not interleave. Keep it as small as the invariant allows: compute outside the lock, mutate inside it. A lock held across a network call is the classic design bug — the lock’s hold time becomes the remote service’s latency, so a slow dependency turns into a queue on your own mutex and then into a thread-pool exhaustion.
Mutex, RWMutex, spinlockA mutex parks the thread in the kernel when contended (cheap uncontended, a syscall when not). A spinlock burns CPU waiting and only wins when the hold time is shorter than a context switch. A reader–writer lock lets readers share — and is often slower than a plain mutex, because it does more bookkeeping and can starve writers. Reach for it only when reads are long and genuinely dominant.
SemaphoreA counter of permits. It is the right primitive for “at most N concurrent” — connection pools, bounded fan-out, in-process rate limits — and it is what most homemade rate limiters should have been. A binary semaphore is not quite a mutex: a mutex has an owner and can be reentrant or track priority, a semaphore is just a number and can be released by a thread that never acquired it.
Condition variableWait until a predicate becomes true, releasing the lock while you wait. Always in a while loop, never an if: spurious wakeups are real, and even without them another thread may have consumed the condition between the signal and your reacquisition. Notify-one versus notify-all is a correctness question when waiters are waiting for different predicates.
DeadlockFour conditions must all hold: mutual exclusion, hold-and-wait, no preemption, circular wait. Break any one and it cannot happen; in practice you break the circular wait by always acquiring locks in the same global order, which is the answer interviewers want. Timeouts turn a deadlock into a livelock or an error rather than removing it, and databases resolve theirs by detecting the cycle and killing a victim — which means your code must retry.
Livelock and starvationLivelock: everyone is busy responding to each other and nothing progresses — two threads politely backing off in lockstep. Starvation: progress happens but not for you, because the lock is unfair or your priority is low. Randomised backoff fixes the first; a fair or queueing lock fixes the second, at a throughput cost.
Atomics and compare-and-swapA single word read-modified-written indivisibly by the hardware. CompareAndSwap(old, new) fails if anyone else moved it, so the caller loops: read, compute, try, retry. That loop is the basis of every lock-free queue and counter. Its famous hazard is ABA — the value returned to its old bit pattern while you were away, so the swap succeeds on a pointer that has been freed and reallocated; the fix is a version tag alongside the pointer.
The memory modelCompilers and CPUs reorder; a write may sit in a store buffer indefinitely, so another core can read a value that changed minutes ago. What makes a write visible is a happens-before edge, and those come from synchronisation: releasing a mutex, an atomic store with release ordering, starting or joining a thread. This is why a plain loop polling a flag can spin forever, and why volatile in Java (which does establish ordering) is a different keyword from volatile in C (which does not).
Thread poolsA bounded number of workers in front of a queue. The queue must be bounded too: an unbounded queue converts a latency problem into an out-of-memory crash, having first made every queued request time out anyway. The interesting design questions are the rejection policy when it is full — shed, block the caller, or run it on the caller’s thread — and keeping separate pools so that a slow dependency cannot starve everything else.
Async and event loopsConcurrency without threads: one thread, a readiness notifier, and a state machine per in-flight operation. Enormously efficient for I/O-bound work, and it has exactly one failure mode — any blocking call, including a CPU-heavy loop or an accidental synchronous file read, stalls everything. Which is why async runtimes ship a separate pool for blocking work, and why “is this function async all the way down” is a real question.
CSP and actorsShare memory by communicating instead of communicating by sharing memory. Go’s channels pass values between goroutines; the actor model gives each actor a mailbox and private state. Both replace “which lock protects this field” with “who owns this value now”, which is a question humans are better at. Neither removes the need to think about buffering, backpressure and shutdown.
ImmutabilityThe cheapest concurrency strategy: if nothing can change, nothing needs protecting. Persistent data structures share unchanged parts between versions so a “copy” is O(log n) rather than O(n); copy-on-write does the same trick for the read-mostly case. The cost is allocation and garbage, which is a real trade rather than a free win.
Producer and consumerThe archetype: a bounded queue between fast producers and slower consumers. Bounded is the whole point — when it fills, the producer must block, drop, or shed, and choosing between those three is backpressure. A system with no backpressure does not fail gracefully under overload, it fails suddenly and everywhere at once.
Thread safety of your own types“Is a hash map thread-safe?” is really “what does your language guarantee, and what does it cost?” Java has HashMap (not safe, and corrupts rather than merely racing), Collections.synchronizedMap (one lock) and ConcurrentHashMap (striped, and its iterators are weakly consistent). Go maps panic on concurrent write by design. Python’s dict is safe only incidentally, by the interpreter lock.
Parallel versus concurrentConcurrency is a structuring technique — dealing with many things at once, which is useful on one core. Parallelism is an execution property — doing many things at once, which needs several. A single-threaded event loop is concurrent and not parallel; a data-parallel numeric loop is parallel and barely concurrent. Interviewers ask for this because it predicts whether you will reach for threads when the problem is I/O.
Topic · 11

The network, from the frame to the request

“What happens when you type a URL and press enter” is still asked because it is unboundedly deep: you can answer it in thirty seconds or forty minutes, and where you stop tells the interviewer your level.

ONE REQUEST, WRAPPED FIVE TIMES ethernet frame · MTU 1500 IP packet · 20 B · addresses, TTL TCP segment · ports, seq, ack, window TLS record · encrypted and authenticated GET /v1/links/abc123  Host: sho.rt Exceed the MTU and you fragment, or the packet is dropped outright if DF is set. Routing, TTL, and the NAT rewriting your source address on the way out. Retransmission, congestion window, and head-of-line blocking for everything above. Certificate chain, SNI, session resumption. Methods, status codes, headers, keep-alive. HTTPS / TCP DNS TCP SYN TLS 1.3 request 4 round trips before the first byte HTTP/3 / QUIC DNS QUIC + TLS request 3 — or 2 on a resumed connection On a 100 ms transatlantic link those two saved round trips are 200 ms of the user’s time, before your handler has run a single line.
This is why “add a CDN” is a latency answer and not just a bandwidth one. Terminating TLS near the user removes round trips from the expensive leg of the journey; the bytes still cross the ocean, but the handshakes do not. It is also why connection pooling matters so much server-side: a pooled connection has already paid for all of the boxes above except the last one.
The layers, used as a mapLink, network, transport, application — enough to say where a problem lives, not to recite seven OSI names. The value is in triage: “the name resolves but nothing connects” is network or firewall; “it connects then hangs” is transport or TLS; “it returns 502” is application. Each layer only knows about the one below it, which is exactly why the failure messages are so unhelpful.
IP addressingCIDR notation is a prefix length: 10.0.0.0/16 is 65,536 addresses, /24 is 256, and each subnet loses a few to the network, broadcast and cloud-reserved addresses. Private ranges (10/8, 172.16/12, 192.168/16) are not routable on the internet, which is what NAT exists to paper over. Sizing a VPC too small is a decision you cannot easily take back, which is why the question gets asked.
DNSA recursive resolver walks root, TLD, then your authoritative server, and everything caches for the TTL. Consequences: a record change is not live everywhere until the old TTL expires, so you lower the TTL before a migration, not during it. A, AAAA, CNAME (which cannot sit at a zone apex), MX, TXT, SRV. And “it works on my laptop” is often a stale cache or a different resolver.
TCPThree-way handshake, sequence numbers, cumulative acknowledgement, retransmission on loss. The window is the throughput limit: bandwidth-delay product means a long fat link needs a big window or it idles. Slow start ramps up, congestion control backs off on loss. TIME_WAIT holds a socket for two minutes after close so a stray packet cannot poison a new connection — and it is why a load generator runs out of ports before the server runs out of anything.
UDP and QUICUDP is a datagram with no ordering, no retransmission and no handshake — the right base when you would rather have the next sample than a resend of the last one. QUIC then rebuilt ordering, congestion control and TLS on top of it in user space, which is the interesting part: it gets streams that do not head-of-line block each other, a one-round-trip handshake, and connections that survive a change of IP address.
TLSThe handshake agrees a cipher, proves identity with a certificate chain up to a trusted root, and derives session keys. SNI names the host you want before encryption starts, which is how one address serves many certificates. TLS 1.3 cut it to one round trip. Practical failures are nearly always a chain missing an intermediate, an expired certificate, a clock that is wrong, or a container with no root store.
HTTP versions1.1 has one request in flight per connection, so browsers open six and everything else queues — head-of-line blocking at the application layer. HTTP/2 multiplexes many streams over one connection and compresses headers, which moves the blocking down to TCP: one lost packet stalls every stream. HTTP/3 over QUIC fixes that by giving each stream its own delivery. Knowing which blocking each version removes is the whole answer.
HTTP semanticsMethods and what they promise: GET safe, PUT and DELETE idempotent, POST neither. Status codes as a contract — 400 you sent nonsense, 401 you are not authenticated, 403 you are but may not, 404 no such thing (or you may not know), 409 conflict, 422 understood but unprocessable, 429 slow down, 502/503/504 the failure is ours. Caching headers, cookies, CORS preflights, and content negotiation all live here too.
Socketsbind, listen, accept — and the accept backlog, which is the queue of completed handshakes waiting for your code to pick them up. When it overflows, the kernel drops connections silently and the client sees a timeout rather than a refusal. A connection is a four-tuple, which is why one client can hold many connections to one server, and why port exhaustion is a client-side problem.
Load balancingL4 forwards packets by address and port — fast, protocol-agnostic, and blind to what a request is. L7 terminates the connection and reads the request, so it can route by path, retry safely, and split traffic by header, at the cost of being in the data path. Then the real questions: what makes a health check meaningful, and does the balancer drain connections on deploy or cut them?
ProxiesForward proxies act for the client, reverse proxies act for the server, sidecars act for one process. What matters in an interview is the consequences: where TLS terminates (and therefore who can read the body), who is allowed to retry (and whether that multiplies load), and how the real client address survives — which is what X-Forwarded-For is for and why trusting it blindly is a security bug.
WebSockets and server-sent eventsBoth give you server-to-client push. A WebSocket upgrades to a bidirectional frame protocol; SSE is a long-lived HTTP response of text events, which is simpler, reconnects itself and only goes one way. The operational cost is the same and it is the interesting part: long-lived connections break assumptions in load balancers, make deploys disruptive, and turn concurrency into a memory-per-connection problem.
gRPC and HTTP/2 streamsA schema-first RPC over HTTP/2: generated clients, binary framing, four call shapes including bidirectional streaming, and deadlines that propagate across hops so a timeout is honoured end to end rather than reset at every service. Costs you browser reach without a proxy and the ability to debug with curl.
Bandwidth, latency, jitter, lossFour different problems that all arrive as “the network is slow”. Bandwidth is fixable with money, latency is bounded by physics and only fixable by moving closer, jitter breaks anything real-time, and loss on TCP looks like latency because of retransmission. Ask which one you are seeing before proposing anything.
The toolsdig for resolution, curl -v for the request and the TLS handshake, ss for socket state, tcpdump when you need the packets, traceroute and mtr for the path and where the loss starts. Naming the right one for a given symptom is half of the credit in a debugging round.
  • What happens when you type a URL and press enter?whether you can walk the whole path — cache, DNS, ARP, route, handshake, TLS, request, render — and stop at the depth the role needs.
  • The service is fine but users say it is slow. Where do you look?splitting client, network and server time; the words TTFB, round trip and percentile.
  • Why would a request succeed from your laptop and fail from the pod?DNS, egress rules, security groups, proxies, certificate trust stores, IPv6 — a list, then a way to bisect it.
  • TCP or UDP for this?whether you reason from the requirement (ordering, loss tolerance, latency) rather than from a memorised table.

These have a page of their own too

Latency, queueing, backpressure and metastable failure are worked through in The Machine Room; the network path from a hostname to a running container is From Account to Pod. Both are in the rail below. This part is the checklist you should be able to tick before either page feels easy.

Part four

Data: modelling it, indexing it, keeping it correct

Databases are the densest source of interview questions per minute of preparation, because almost every backend role touches them and almost every candidate’s knowledge stops at the ORM. Four things get asked: can you model a domain, can you make a query fast and say why, do you understand what a transaction promises, and do you know what breaks when one machine is no longer enough.

Topic · 12

Relational modelling and the SQL you must be able to write

The schema question is a design question in miniature: entities, relationships, cardinality, and where the truth lives. The SQL question is a fluency test — you will be asked to write it, not to describe it.

Entities and relationshipsOne-to-one (usually a sign the two tables should be one), one-to-many (a foreign key on the many side), many-to-many (a join table, which almost always grows attributes of its own — a memberships table wants a role and a joined-at). Draw the boxes and the cardinalities before naming a single column; half of all bad schemas are a many-to-many that someone modelled as a comma-separated string.
KeysA surrogate key is meaningless and never changes; a natural key is meaningful and eventually does, because email addresses change and country codes get reassigned. Default to a surrogate primary key with a unique constraint on the natural one, so you get both. Sequential integers pack better in an index and leak your volume; random UUIDv4 leaks nothing and scatters writes across the whole B-tree, which is why UUIDv7 — time-ordered — exists.
Normal forms, then when to stop1NF: no repeating groups or arrays pretending to be columns. 2NF and 3NF: every non-key column depends on the key, the whole key, and nothing but the key — which in practice means one fact stored in one place. Denormalise afterwards, deliberately, for a measured read, and then own the write-side cost: every copy needs a way to be kept correct, and “we will remember to update both” is not one.
ConstraintsNOT NULL, UNIQUE, CHECK, foreign keys, defaults. The database is the last place an invariant can actually be enforced — application code runs in ten processes, gets bypassed by a migration script, and does not exist for the analyst with a psql prompt. A partial unique index (UNIQUE ... WHERE deleted_at IS NULL) is the trick that makes “unique among the live rows” expressible.
JoinsInner keeps matches; left keeps every row on the left and fills the right with NULLs. Two things to watch: a join to a table with duplicate keys multiplies rows, which silently doubles every SUM downstream; and a filter on the right table in the WHERE clause quietly converts a LEFT JOIN into an INNER one, which is why that predicate belongs in the ON. Semi and anti joins are what EXISTS and NOT EXISTS compile to.
AggregationGROUP BY collapses rows; HAVING filters after the collapse, WHERE before it — putting the filter in the right one is both a correctness and a performance question. Every selected column must be grouped or aggregated, which is an error rather than a silent average, and COUNT(*), COUNT(col) and COUNT(DISTINCT col) are three different questions: the second skips NULLs, the third is expensive.
Window functionsAggregate without collapsing: each row keeps its identity and gains a value computed over a PARTITION BY and an ORDER BY. ROW_NUMBER, RANK and DENSE_RANK differ only in how they treat ties; LAG and LEAD reach to neighbouring rows for deltas; a running total is SUM(...) OVER (ORDER BY ...). The rule that catches people: windows are computed after WHERE and GROUP BY, so you cannot filter on one without a subquery or CTE.
Subqueries and CTEsA correlated subquery runs per outer row and is the usual reason a report takes four minutes. EXISTS stops at the first match and handles NULLs sanely; IN with a subquery containing a NULL returns nothing at all under NOT IN, which is a genuinely surprising bug. Recursive CTEs walk trees and graphs — org charts, category paths, dependency chains — and need a termination guard or they do not stop.
NULL semanticsThree-valued logic: NULL = NULL is not true, it is unknown, so comparisons need IS NULL. It propagates through arithmetic, it is skipped by aggregates but counted by COUNT(*), and it is ignored by most unique constraints — so a UNIQUE column happily holds many NULLs. Deciding what NULL means in each column (absent? not applicable? not yet known?) is a modelling decision, and three meanings in one column is a bug.
Set operationsUNION deduplicates and therefore sorts the whole result; UNION ALL concatenates and is what you almost always meant. INTERSECT and EXCEPT are readable but often slower than the equivalent join or anti-join. If a query got mysteriously slow after someone added a branch, look for the missing ALL.
Data typesMoney in integer minor units or NUMERIC, never a float. Timestamps with time zone stored as UTC, formatted at the edge — and remember that a “date” without a zone is ambiguous for anyone not in yours. Text over varchar(n) in Postgres, because the length limit is a constraint pretending to be a type. Enums are cheap until you need to remove a value. A JSON column is the right answer for genuinely open-ended attributes and the wrong one for a schema you have not bothered to design.
Soft deletes and historyA deleted_at column means every query must remember to filter, forever, and one that forgets is a data leak. Alternatives: move the row to an archive table, keep an append-only audit log, or model the whole thing as events with the current state derived. Pick based on who needs the history and for how long — regulators, support staff and analysts want different things.
Schema migrationExpand, migrate, contract, spread over several deploys: add the new column nullable, write to both, backfill in batches, switch reads, stop writing the old one, drop it. Each step is individually reversible, which is the whole point, because old and new code are live at the same moment during a rolling deploy. Adding a column with a non-constant default, or an index without CONCURRENTLY, takes a lock on a big table and stops the site.
ORMsThey generate SQL you should be able to read. The N+1 problem — one query for the list and one per row for its children — is the standard finding, and the fix is an eager-loading directive or a join. Lazy loading outside a session throws; identity maps cache within one; and every ORM has an escape hatch to raw SQL that you should use without embarrassment for reports.

The one query that shows up most often is “top N per group”, and the good answer uses a window function rather than a correlated subquery. Being able to write it without hesitating is worth more than knowing three ways to do it slowly.

-- top_per_group.sql — the most-asked "senior SQL" shape.
-- "The three most recent orders for every customer, with the running total."
WITH ranked AS (
    SELECT
        o.customer_id,
        o.id,
        o.placed_at,
        o.amount_cents,
        ROW_NUMBER() OVER (PARTITION BY o.customer_id
                           ORDER BY o.placed_at DESC)        AS rn,
        SUM(o.amount_cents) OVER (PARTITION BY o.customer_id
                                  ORDER BY o.placed_at)      AS running_cents
    FROM orders o
    WHERE o.placed_at >= now() - interval '90 days'
)
SELECT customer_id, id, placed_at, amount_cents, running_cents
FROM ranked
WHERE rn <= 3                       -- filter AFTER the window has been computed
ORDER BY customer_id, placed_at DESC;

-- Two things an interviewer listens for:
--   * why the rn filter cannot go in the WHERE of the inner query
--     (window functions run after WHERE, so rn does not exist yet), and
--   * that this reads the 90-day slice once, where a correlated subquery
--     would re-scan per customer.
Topic · 13

Indexes, and the two shapes storage engines come in

“Add an index” is not an answer. Which index, on which columns, in which order, and what it costs on write — that is the answer.

THE TWO SHAPES EVERY STORAGE ENGINE IS ONE OF B-TREE — UPDATE IN PLACE  ·  POSTGRES, INNODB root page internal internal leaf leaf leaf leaf LSM TREE — APPEND AND COMPACT  ·  ROCKSDB, CASSANDRA memtable, sorted, in RAM write-ahead log, appended SST level 0 level 1 level 2 Compaction merges files and finally drops the tombstones. A bloom filter per file skips most. OPERATION B-TREE LSM TREE point read three or four page hops, mostly cached memtable, then files newest first random write log it, then rewrite a whole page append; the cost is deferred to compaction range scan leaves are linked, so it is sequential a merge across every level space fragmentation and part-empty pages old versions live until compaction runs Write-heavy and mostly point lookups favours the LSM; read-heavy with ranges and updates favours the B-tree. Say which your workload is.
Both engines pay for a random write, they just pay at different times. The B-tree pays immediately and predictably; the LSM defers it into compaction, which is why an LSM-backed store can absorb a write burst and then get slower an hour later. That deferral is the answer to “why did our p99 spike with no change in traffic?” more often than any application-level cause.

Then the part candidates get wrong: column order in a composite index. An index on (a, b) serves a lookup on a and on a plus b, but not one on b alone — it is a sorted list keyed left to right, exactly like a phone book sorted by surname then first name. Equality columns come first, the range column last, and anything after a range column cannot be used for seeking.

-- index_order.sql — the phone-book rule, made concrete.
CREATE INDEX idx_events_tenant_type_time
    ON events (tenant_id, event_type, created_at DESC);

-- Uses the index fully: equality, equality, then a range on the last column.
SELECT * FROM events
 WHERE tenant_id = $1 AND event_type = 'login' AND created_at > now() - interval '1 day'
 ORDER BY created_at DESC LIMIT 50;

-- Uses only the first column, then filters the rest by hand.
SELECT * FROM events WHERE tenant_id = $1 AND created_at > $2;

-- Cannot use it at all: the leading column is missing.
SELECT * FROM events WHERE event_type = 'login';

-- A covering index answers from the index alone -- no heap fetch at all:
CREATE INDEX idx_events_cover ON events (tenant_id, created_at DESC) INCLUDE (event_type, payload_id);

Being able to read a plan is what turns “add an index” from a guess into a diagnosis. Read it innermost first, and compare the estimated row count against the actual one before you look at anything else — if those disagree by orders of magnitude, the planner was working from bad statistics and every choice above that line is suspect.

-- plan.sql — EXPLAIN (ANALYZE, BUFFERS) on the query from above.
Limit  (cost=0.56..842.11 rows=50 width=48) (actual time=0.042..0.310 rows=50 loops=1)
  ->  Index Scan using idx_events_tenant_type_time on events
        (cost=0.56..16842.77 rows=1000 width=48)
        (actual time=0.040..0.301 rows=50 loops=1)
        Index Cond: ((tenant_id = $1) AND (event_type = 'login')
                     AND (created_at > (now() - '1 day'::interval)))
        Buffers: shared hit=54
Planning Time: 0.180 ms
Execution Time: 0.338 ms

-- What each line is telling you:
--   Index Scan          the index was used to SEEK, not merely to filter
--   Index Cond          all three predicates went INTO the index -- if one had
--                       appeared as "Filter:" instead, the index stopped being
--                       useful at that column and the rest was checked by hand
--   rows=1000 vs 50     the estimate was 20x out; harmless under a LIMIT, and a
--                       warning sign anywhere a join sits above it
--   Buffers: hit=54     54 pages, all from cache. "read=" would mean real I/O
--   loops=1             on the inner side of a nested loop this is per outer
--                       row, so multiply before believing the actual time

-- The same query with no usable index, on ten million rows:
--   Seq Scan on events  (actual time=0.011..1841.220 rows=50 loops=1)
--     Filter: (...)     Rows Removed by Filter: 9999950   <- the tell
B-tree indexThe default, and the only one that serves ranges, prefix matches and ordering as well as equality. Three or four levels deep for very large tables, with everything but the leaf usually cached, so a point lookup is one or two real reads. It also satisfies ORDER BY without a sort, which is often the bigger win: it removes the memory-hungry step, not just the scan.
Hash indexEquality only — no ranges, no ordering, no prefix. Marginally faster and smaller than a B-tree for exact lookups on long keys, and almost never worth giving up the other capabilities for. Worth knowing exists so you can say why you are not using one.
Composite index and column orderAn index on (a, b, c) is a phone book sorted by surname, then first name, then middle name: it serves a lookup on a, on a, b, and on a, b, c — but never on b alone. Equality columns first, the range column last, because everything after a range can no longer be used for seeking, only for filtering. Getting this order wrong is the single most common indexing mistake.
Covering indexWhen the index contains every column the query touches, the table itself is never read — an index-only scan. Usually the largest single win available, and Postgres spells it INCLUDE so the extra columns are stored in the leaves without being part of the key. The trade is width: a wide index is a bigger index, and every write pays for it.
Partial indexIndex only the rows you actually query — WHERE status = 'pending', WHERE deleted_at IS NULL. On a table where the interesting rows are 1% of the total, the index is 1% of the size, stays in cache, and costs nothing to maintain for the other 99% of writes. Combined with UNIQUE it is how you express “unique among the live rows”.
Unique indexA constraint and an index at once, and the only sound way to enforce “at most one” under concurrency — a SELECT then INSERT in application code is a check-then-act race that will fire in production and never in testing. Let the write fail and catch the violation; that is the pattern, not a fallback.
Full-text and trigramLIKE 'foo%' can use a normal B-tree because it is a prefix; LIKE '%foo%' cannot use one at all, because the sort order tells you nothing about infixes. What does work is a trigram index for fuzzy and infix matching, or a full-text index with stemming, ranking and stop words. Knowing why the leading wildcard is fatal is the actual question.
GIN, GiST and geospatialInverted indexes over things that are not scalars: array containment, JSON keys, full-text lexemes, and shapes with R-tree-like structures for “within this box” and “nearest to this point”. Slower to write, sometimes much larger than the data, and the only way to make those queries anything but a full scan.
Clustered versus secondaryInnoDB stores the whole row inside the primary key’s B-tree, so the primary key is the table — and every secondary index stores the primary key as its pointer, meaning a secondary lookup costs a second descent. That is why a wide or random primary key in MySQL is expensive twice over. Postgres keeps a separate heap and indexes point at physical positions, which is a different set of trade-offs and the reason its indexes need vacuuming.
Selectivity and statisticsAn index only helps if it eliminates most rows. On a boolean, or a status column that is 95% one value, a scan is cheaper and the planner knows it — from statistics that are sampled, and that go stale after a bulk load. “It was fast until we imported ten million rows and nobody ran ANALYZE” is a real and common answer to why staging and production differ.
The query planEXPLAIN estimates, EXPLAIN ANALYZE actually runs it and shows both. What to look at, in order: estimated versus actual row counts — a mismatch of orders of magnitude means the statistics are lying and every choice above it is suspect; then the join strategy (nested loop for few rows, hash for many, merge for sorted inputs); then whether a sort spilled to disk. Read it bottom-up, innermost first.
What an index costsEvery insert, update and delete maintains every index on the table, so the write path pays for indexes the read path never uses. They also consume cache that the data wanted. Find the unused ones — every engine exposes usage counters — and drop them; and be suspicious of a table with fifteen indexes, which usually means nobody ever removed one.
Topic · 14

Transactions, isolation and the anomalies each level allows

Everyone can expand ACID. The question that separates people is which anomalies your default isolation level still permits — because it is never none, and the default is rarely serialisable.

WHAT EACH ISOLATION LEVEL STILL LETS HAPPEN dirty read non-repeatable phantom lost update write skew read uncommitted possible possible possible possible possible read committed prevented possible possible possible possible repeatable read prevented prevented possible* possible possible snapshot prevented prevented prevented prevented possible serialisable prevented prevented prevented prevented prevented * MySQL InnoDB blocks most phantoms at repeatable read with gap locks; the SQL standard does not require it. Postgres defaults to read committed and its “repeatable read” is the snapshot row. MySQL defaults to repeatable read.
Write skew is the one people have never heard of, and the one that bites. Two transactions each read the same rows, each check a rule that still holds, and each write a different row — so no conflict is detected and the rule is now broken. The classic is two doctors both cancelling their on-call shift when at least one must remain. Snapshot isolation permits it; only serialisable stops it, or an explicit lock on the thing you are actually protecting.

Write skew is worth seeing rather than reading about, because every step in it succeeds. Two transactions read the same rows, each checks a rule that still holds, and each writes a different row — so nothing conflicts, nothing is detected, and the rule is now broken.

-- write_skew.sql — the on-call rule: at least one doctor stays on shift.
-- Both transactions run at REPEATABLE READ (snapshot isolation). Both commit.

-- transaction A (Alice)                  -- transaction B (Bob)
BEGIN;                                    BEGIN;
SELECT count(*) FROM shifts               SELECT count(*) FROM shifts
 WHERE on_call = true;   -- 2              WHERE on_call = true;   -- 2, same snapshot
-- 2 > 1, so it is safe to leave          -- 2 > 1, so it is safe to leave
UPDATE shifts SET on_call = false         UPDATE shifts SET on_call = false
 WHERE doctor = 'alice';                   WHERE doctor = 'bob';
COMMIT;                                   COMMIT;
-- Different rows, so no write conflict. Nobody is on call.

-- Three fixes, in increasing order of cost:
--   1. Lock what you are actually protecting, not what you are writing:
SELECT count(*) FROM shifts WHERE on_call = true FOR UPDATE;   -- serialises them
--   2. Make the invariant a constraint the database can check, so it cannot
--      be violated by any interleaving at all (a materialised counter row
--      with a CHECK, or a single row holding the count).
--   3. SERIALIZABLE, and then handle serialisation_failure by retrying:
BEGIN ISOLATION LEVEL SERIALIZABLE;   -- one of the two will be aborted
ACID, and which letters you actually getAtomicity: all or nothing, delivered by the log and rollback. Consistency: your constraints hold at commit — the database only enforces what you declared, so this letter is mostly yours. Isolation: the one that is configurable and therefore the one worth talking about. Durability: committed means survives a crash, which is only true if fsync reached a disk, so replication mode and disk cache settings quietly redefine it.
MVCCEach transaction sees a snapshot, so readers never block writers and writers never block readers — which is why modern databases feel so much better than lock-based ones. The cost is that old row versions must live until no snapshot needs them: Postgres calls the cleanup VACUUM, and a single long-running transaction holds the horizon back and bloats every table on the instance. That is the mechanism behind “an idle transaction filled the disk”.
LockingShared for reads, exclusive for writes; row-level normally, table-level for DDL. MySQL adds gap and next-key locks that cover the space between rows, which is how it blocks phantoms at repeatable read — and also why an unindexed DELETE ... WHERE can lock far more than the rows it deletes. Optimistic (a version column and a conditional update) versus pessimistic (FOR UPDATE) is a throughput-versus-contention choice you should be able to argue either way.
DeadlockTwo transactions each holding what the other needs. The database detects the cycle and kills a victim with a specific error; it does not hang. So the application must retry, with backoff, and that retry is the piece missing from most codebases. Prevention is the same rule as in-process: touch rows in a consistent order — sort the ids before you update a batch and most of it disappears.
SELECT ... FOR UPDATETakes the row lock now so a read-modify-write cannot interleave, at the cost of serialising everyone who wants that row. Add NOWAIT to fail fast rather than queue, or SKIP LOCKED to walk past locked rows — which is how you build a work queue in a relational database, and a genuinely good answer when someone asks for one.
Optimistic concurrencyRead the row with its version, compute, then UPDATE ... WHERE id = ? AND version = ? and check that one row was affected. Zero rows means somebody else got there first, so re-read and retry. Scales far better than a held lock when conflicts are rare, and is strictly worse when they are common — say which you expect.
Write-ahead logThe change is written to a sequential log and flushed before the page is touched, so a crash can be replayed forward from the last checkpoint. That one mechanism gives you durability, crash recovery, physical replication, change data capture and point-in-time restore — which is why “how does replication work” and “how does a database survive a power cut” have the same answer.
Long transactionsThey hold locks, hold back the vacuum horizon, and keep a connection out of the pool. The specific rule worth stating: never keep a transaction open across a network call. A transaction that begins, calls a payment provider, and then commits has made an external service’s p99 into your lock hold time, and its timeout into your outage.
Isolation in practicePostgres defaults to read committed; each statement gets a fresh snapshot, so two reads in one transaction can differ. MySQL InnoDB defaults to repeatable read, where the snapshot is taken once per transaction. Your ORM opens one of these without telling you, and it almost never retries a serialisation failure on your behalf — if you ask for serialisable, you have signed up for a retry loop.
Distributed transactionsTwo-phase commit gives you atomicity across services and a coordinator whose failure leaves participants blocked holding locks, which is why almost nobody runs it. The usual answer is a saga: a sequence of local transactions, each with a compensating action, so “undo” is a business operation (refund, release, cancel) rather than a rollback. Be ready to say what a compensation cannot undo — an email that has been sent.
IdempotencyThe application-level answer to “the connection dropped, did the write happen?”. A client-supplied key, a unique index on it, and a stored response: the second attempt hits the constraint, you look up what you returned the first time, and you return it again. Three lines that make every retry in the system safe, and the reason retries are not a data-corruption strategy.
The outbox patternWriting a row and publishing an event are two systems and cannot be one transaction — whichever you do second can fail, and then they disagree forever. Instead write the event into an outbox table in the same transaction as the row, and let a separate process read it and publish, at-least-once. The consumers were already required to be idempotent, so nothing new is asked of them.
What your framework does by defaultWhich isolation level it opens, whether it wraps a whole HTTP request in one transaction, whether a nested call joins the outer transaction or starts its own, and whether a caught exception has already marked the whole thing rollback-only. Every one of those has produced a production incident in some codebase, and being able to name your framework’s answers is exactly the depth the question is probing.
Topic · 15

When one machine is not enough

Replication, partitioning and everything they break. This is the bridge between the database questions and the system-design round, and interviewers use it exactly that way.

Vertical firstA bigger machine is the correct first answer far more often than candidates dare to say — a single Postgres instance on modern hardware handles tens of thousands of transactions a second and terabytes of data. Say when it stops: when you exceed the largest instance available, when a single write path saturates one core, when the blast radius of one machine becomes unacceptable, or when the restore time from backup no longer meets the objective.
Read replicasThe cheapest real scaling move, because most workloads are 100:1 reads. What you buy is read capacity; what you pay is lag — the replica applies the log some milliseconds or seconds behind, and under a heavy write burst that becomes minutes. Route reports and search there; keep anything the user just changed on the primary.
Read-your-writesThe weakest guarantee that stops users thinking the product is broken: after I save, I see my change. Three implementations worth naming — send the user to the primary for a short window after a write, pin that user to one replica, or carry a log position with the request and wait for the replica to catch up to it. Pick one and say why; “eventual consistency is fine” is not an answer to a user who just edited their profile.
Synchronous versus asynchronous replicationAsync: the primary commits and tells you it is done, so a failover can lose the last few transactions — a non-zero recovery point objective. Sync: the primary waits for a replica, so nothing is lost and every commit now costs a network round trip, and a sick replica can stall all writes. Semi-synchronous (wait for one of several) is the usual compromise, and quorum commit is the same idea generalised.
FailoverWho notices, who promotes, and what the clients do during the gap. Automatic failover risks split brain — two primaries accepting writes — which is why fencing exists: the old primary must be shot or blocked before the new one accepts anything. Client behaviour matters as much: connections must be dropped and re-resolved, and in-flight transactions must fail rather than silently retry against a different node.
PartitioningSplitting one table by a key, inside one database. By range (dates, ids) keeps scans efficient and creates a hot partition for “today”; by hash spreads evenly and destroys range scans; by list suits a fixed set like region. The everyday win is not performance but maintenance: dropping last year’s partition is instant where deleting the rows is an all-night job.
ShardingThe same split across separate machines, and a different problem, because now the routing is yours. Pick a key you can live with for five years — usually the tenant or the user — because cross-shard joins and transactions are the price and they do not get cheaper. The question that exposes real experience: what do you do about the query that has no shard key?
Consistent hashingNaive hash(key) % n remaps almost everything when n changes. Placing nodes and keys on a ring and walking clockwise moves only 1/n of the keys when a node joins or leaves. Virtual nodes — each physical node appearing at many ring positions — are what make the distribution even and let you weight machines differently.
Hot keys and hot partitionsTable-level capacity is a fiction the moment one key is popular: one celebrity, one tenant, one status='pending' value. Fixes, in order of preference: cache it in front, split the key with a salt suffix and fan out reads, or model the hot thing differently altogether. Detection matters too — without per-key metrics it just looks like the database is slow.
RebalancingMoving data while serving traffic, usually the hardest part of any sharding answer and the part candidates skip. Double-write to old and new, backfill in batches, verify, switch reads, stop the old write. Fixed shard counts with many more logical shards than machines make this far easier, which is why systems that expect to grow start with 1024 logical shards on four boxes.
Connection poolingA database has a hard connection ceiling — each one is a process or a thread with a memory cost — and fifty pods with a pool of twenty each is a thousand connections asking for a hundred. The fix is a pooler in front (transaction-mode pooling multiplexes far better than session-mode), plus honest per-service pool sizing. This is the most common way a well-designed service takes down a shared database.
Backups and restoreA backup nobody has restored is a hypothesis. Full snapshots plus write-ahead log archiving give point-in-time recovery; the numbers to state are RPO (how much you accept losing) and RTO (how long recovery may take), and the second is measured by an actual rehearsal. Also: a backup that a compromised account can delete is not a backup against ransomware.
Multi-regionActive–passive is a warm copy and a failover procedure. Active–active means accepting writes in two places, which means conflicts, which means a resolution policy — last-write-wins silently loses data, so it needs to be a decision rather than a default. Underneath everything, the speed of light: a cross-Atlantic round trip is about 100 ms, and no amount of architecture removes it from a synchronous write.
Change data captureRead the replication log and stream committed changes out to caches, search indexes and warehouses. It replaces dual writes, so those systems cannot disagree with the database; it gives you the order the database committed in; and it costs you a pipeline to operate plus the fact that consumers see changes after the fact, not as part of the transaction.
Topic · 16

The other stores, and when each is the right answer

“Which database would you use?” is a trap only if you answer with a product. Answer with an access pattern and the product follows.

FamilyUse it whenWhat it costs you
RelationalThe data has relationships, you need transactions, and queries will change.Scaling writes past one machine; schema changes on huge tables.
Key–valueYou always know the key, and the value is opaque.No queries you did not plan for; no joins at all.
Wide columnEnormous write volume, queries fixed at design time.The partition key is forever, and modelling is per query.
DocumentAggregates read and written whole; a schema that varies by tenant.Duplication, and consistency across documents is yours to keep.
Search indexText relevance, facets, fuzzy matching, aggregations over free text.Eventual consistency with the source of truth, and reindexing.
CacheReads dominate, and staleness is acceptable and bounded.Invalidation, stampedes, and a cold start that can take the system down.
Time seriesAppend-only measurements, queried by range and rolled up.Cardinality explodes if a label is user-controlled.
GraphThe query is about paths and neighbourhoods, not rows.An operational niche; most graph questions are answered fine by SQL.
Object storeLarge immutable blobs, cheap and durable, served by URL.Latency, listing costs, and no partial updates.
Columnar warehouseAnalytics over billions of rows, few columns at a time.Not for a request path; loads in batches, answers in seconds.
Log / streamOrdered, replayable events consumed by many independent readers.Retention, partition ordering only, and consumer lag as a new thing to watch.
EmbeddedSingle process, no network hop, local state.It is not shared, and it dies with the process unless you plan otherwise.
  • SQL or NoSQL for this?whether you name the access patterns first. The only wrong answer is one given before the queries are known.
  • Your read replica is 30 seconds behind. What do users see?concrete symptoms — a profile edit that reverts, a payment that seems to vanish — and then a fix that matches the requirement.
  • How would you shard this?a key with even distribution and no cross-shard transactions in the hot path, plus an honest account of the queries it ruins.
  • The same query is fast in staging and slow in production. Why?data volume, statistics, plan choice, cache state, contention — and EXPLAIN ANALYZE on both.
Part five

System design

The round with no right answer and a great many wrong ones. You are given three sentences and forty-five minutes, and the grade comes from whether you turned the ambiguity into a set of decisions, each with a reason and a cost. The subject matter is finite — a dozen building blocks, a page of numbers, four consistency ideas and a catalogue of problems that reuse them.

Topic · 17

The method, and the clock it runs on

Every good design round has the same six phases in the same order. Skipping the first two is the most common way to fail, and it is invisible until minute thirty when nothing can be justified.

FORTY-FIVE MINUTES, SIX PHASES 1 requirements 0–5 min who, what, how many, and what is out of scope 2 the envelope 5–10 min requests per second, bytes stored, bandwidth 3 API and data 10–18 min the four endpoints and the three tables 4 the diagram 18–30 min boxes, arrows, and one request end to end 5 the deep dive 30–40 min whichever box they point at, to real depth 6 failure and ops 40–45 min what breaks, what you watch, what you page on HOW THE SAME FORTY-FIVE MINUTES GETS WASTED boxes at minute two Nothing drawn afterwards can be justified, because nothing was required. everything, equally Twelve components at one sentence each reads as no depth anywhere. no failure story The single most common senior miss: a design where nothing ever breaks. Say the phase you are entering out loud. It tells the interviewer you have a method, and it lets them redirect you cheaply.
Phase five is where the level is decided. Phases one to four can be rehearsed and mostly look the same across candidates; the deep dive cannot. When the interviewer taps a box and asks “how does this actually work?”, they are checking whether the diagram was drawn from understanding or from memory of a diagram. Have one box on your canvas you could talk about for ten minutes, and steer the dive there.
  1. Functional requirements, written as a list. Three to five bullet points, agreed out loud. “Shorten a URL, resolve one, expire them, count clicks — is analytics in scope?”
  2. Non-functional requirements, with numbers. Scale, latency target, availability target, consistency needs, retention, cost sensitivity. Ask for them; invent them if the interviewer will not give them; either way, write them down.
  3. Envelope estimates. Requests per second at peak, storage per year, bandwidth. They decide whether one machine or a hundred, and everything after depends on that answer.
  4. The interface. The endpoints or the events, with request and response shapes. This is the contract the rest of the design serves.
  5. The data model. Tables or documents, the primary key, the access patterns, and the index that supports each one.
  6. The diagram, then one request walked through it. Draw it, then narrate a single write and a single read from the client to storage and back.
  7. Bottleneck, then scale it. Find the first thing that breaks, fix that one thing, then look again. Scaling everything at once is not a plan.
  8. Failure and operations. Each component dies in turn: what does the user see, what recovers it, what do you alert on. Finish here even if you have to cut something else short.
Topic · 18

The numbers you should not have to derive

Estimation is not arithmetic under pressure, it is recall plus one multiplication. These are the values worth knowing cold; everything else you can get from them in a line.

BACK OF THE ENVELOPE, FROM MEMORY TRAFFIC AND STORAGE WHAT ONE MACHINE DOES seconds in a day 86 400, call it 10^5 1 million events a day about 12 per second 1 billion requests a day about 12 000 per second peak against average two to three times 1 KB rows, a billion of them 1 TB, before indexes typical read to write ratio 100:1, often worse a year of daily volume multiply by 365 a UUID and a timestamp 16 bytes and 8 bytes app server, no I/O 10 000 req/s app server, one DB call 1 000–5 000 req/s relational DB, simple writes 5 000–15 000 TPS in-memory store, one node 100 000+ ops/s NVMe solid-state disk 500 MB/s a 10 gigabit network card 1.25 GB/s one log partition, ordered about 10 MB/s web request budget 200–300 ms at p99 Round everything. A billion requests a day is “ten thousand a second, thirty thousand at peak” — and that is precise enough to choose an architecture. The point of the estimate is the decision it unlocks: one box or fifty, one region or three, a cache or no cache. Say which decision each number is for.
State the assumption louder than the arithmetic. “Assume a hundred million daily users, ten actions each” is a decision the interviewer can correct in one word; the multiplication that follows is not interesting and nobody is checking it. Candidates who get this backwards spend four minutes on long division and still have no agreed scale.
Topic · 19

The building blocks, and what each one costs

Every design is assembled from about a dozen parts. Knowing what each buys is table stakes; knowing what each costs is the senior signal, because a design with no costs in it has not been thought about.

THE PATH OF ONE REQUEST, AND WHAT EACH HOP IS FOR client Retries, timeouts and a local cache live here too — and a retrying client is a load multiplier. DNS and CDN Serves the cacheable bytes near the user. Costs you invalidation and a TTL you cannot take back. load balancer Spreads traffic and hides a dead instance. Needs health checks that mean something, and draining. API gateway Authentication, rate limits, quotas, request IDs. One more hop, and one more thing that can be down. stateless service Stateless so you can add another one. Any session state you keep here is a scaling bug in waiting. cache Cuts latency and load. Costs staleness, invalidation, stampedes, and a cold start that can kill you. primary database The source of truth and the thing that is hardest to scale. Every write goes here or it is not real. read replicas Read capacity, bought with lag. Decide which reads may be stale before you draw the arrow. queue or log Moves work out of the response and absorbs a burst. Costs ordering, duplicates, and a lag to monitor. workers Retries, dead letters, poison messages. Handlers must be idempotent because delivery is at-least-once. derived stores Search index, warehouse, materialised views. Rebuildable by definition — say how, and how long it takes. object store Big immutable things, served by signed URL so the bytes never touch your service at all. Cutting across all of it: identity, configuration, rate limiting, observability, and a way to turn a feature off without a deploy.
Add a box only when you have named the thing it fixes. The most common design-round tell is a queue with nothing slow behind it, or a cache in front of a table that is read twice an hour. Each block in the list above is an answer to a specific pressure — latency, load, burstiness, durability, blast radius — and saying which pressure you are relieving turns a generic diagram into a designed one.

Named patterns worth being able to define

horizontal scalingstateless servicecachingcache asideread throughwrite throughwrite behindcache stampedenegative cachingload balancingconsistent hashingsticky sessionsshardingreplicationleader electionquorummessage queuepublish–subscribeevent logfan-out on writefan-out on readidempotency keyoutboxsagaCQRSevent sourcingmaterialised viewrate limitingtoken bucketleaky bucketsliding window countercircuit breakerbulkheadbackpressureload sheddinggraceful degradationretry with jittertimeout budgethedged requestbloom filterCDNedge computeservice discoverysidecarAPI gatewayBFFwebhooklong pollingserver-sent eventsWebSocketblue–greencanaryfeature flagmulti-tenancygeo-replicationcell-based architectureshuffle sharding
Topic · 20

Consistency, ordering and delivery

Four ideas that decide most of the hard follow-up questions: what a reader is promised, what a partition forces, what a message queue really guarantees, and how you make a repeated write harmless.

Strong consistencyEvery read observes the latest committed write, so the system behaves as if there were one copy. It costs coordination — a quorum, a leader, or a lock — and coordination costs a round trip, which is why strong consistency across regions means a hundred milliseconds on every write. Use it where a stale read causes a wrong decision: balances, inventory, permissions, uniqueness.
Eventual consistencyReplicas converge if writes stop. Fine for a like count, a follower total, a search index or a feed; wrong for anything a user will immediately re-read as the truth of what they just did. The useful discipline is to name the window — “replicas are within two seconds under normal load, minutes during a backfill” — because “eventually” on its own is not a design.
Read-your-writesThe weakest guarantee that keeps the product from feeling broken: after I write, I see my write. Implement it by routing a user to the primary for a short window, pinning the session to one replica, or carrying a log position the replica must reach before answering. This is the one to reach for when someone says “the profile edit reverted”.
Monotonic readsTime never runs backwards for a single reader. Broken by round-robin routing across replicas at different lag: you refresh and the comment you just saw disappears. Session stickiness or a client-held version fixes it, and it is a different guarantee from read-your-writes — you can have either without the other.
Causal consistencyIf A happened before B, nobody sees B without A. The natural model for anything threaded — a reply must not appear before the comment it answers, and a photo must not appear before the album that contains it. Cheaper than total ordering because it only constrains related operations, and usually implemented by shipping a version vector or a dependency token with the request.
CAP, and then PACELCDuring a network Partition you must choose between Consistency and Availability — but partitions are rare, and CAP says nothing about the other 99.9% of the time. PACELC completes it: else, when there is no partition, you are still trading Latency against Consistency on every single request. The second half is the one that shapes your architecture; the first half is the one everybody quotes.
QuorumsWith N replicas, requiring W acknowledgements to write and R to read guarantees an overlap when R + W > N. That is the whole trick, and it is tunable per request: W=1 for a fast fire-and-forget write, R=N for a critical read. Note what it does not give you — overlap means you see a copy of the latest write, not that concurrent writes were ordered, which is why quorum systems still need conflict resolution.
Linearisability versus serialisabilityTwo different words that both get called “strong”. Linearisability is about single objects in real time: once a write completes, every later read sees it. Serialisability is about transactions: the result matches some serial order, which need not be the real-time one. A system can be one and not the other, and the combination — strict serialisability — is what distributed SQL databases charge you for.
Delivery semanticsAt-most-once: send and forget, lose some. At-least-once: retry until acknowledged, duplicate some. Exactly-once delivery does not exist on a network — the acknowledgement can be the message that is lost. What does exist is at-least-once delivery plus idempotent processing, or an atomic read-process-write inside one system. Saying that sentence is the answer; claiming exactly-once is the trap.
OrderingTotal order across a whole system is expensive and almost never needed. Per-key order is cheap: partition by the key that must be ordered, and one consumer per partition gives you sequential processing for free. The design consequence is that your partition key and your ordering requirement are the same decision — and that adding partitions later reshuffles keys and breaks the order you were relying on.
IdempotencyA client-supplied key, a unique index on it, and a stored response. The second attempt hits the constraint, you return what you returned the first time, and every retry in the system becomes safe. Decide the key’s scope (per client? per endpoint?) and its lifetime (24 hours is common), and be clear that a natural key — the order id — is better than a random one when you have it.
ClocksWall-clock time drifts, jumps at NTP corrections, and goes backwards; never use it to measure a duration or to order events. Use a monotonic clock for elapsed time, and logical clocks (Lamport, vector) or hybrid ones (a physical timestamp with a logical counter) for ordering. “Last write wins by timestamp” across machines with skewed clocks silently discards the write that actually came second.
Distributed locksA lease with an expiry, so a dead holder does not block forever. The subtlety that separates answers: a process can be paused — by garbage collection, by the scheduler — past its lease expiry and then act as if it still holds the lock. The fix is a fencing token, a number that increases with each grant, which the downstream resource checks and rejects if it has already seen a higher one.
ConsensusRaft and Paxos give a set of machines one agreed sequence of decisions, surviving a minority failure. You rarely implement one and frequently depend on one: leader election, configuration stores, service discovery, and the metadata layer of most distributed databases. The practical takeaways are that it needs a majority to make progress, and that every decision costs a round trip — so the design goal is to need it as rarely as possible.
The dual writeWriting to the database and then publishing an event, with no transaction spanning both. Whichever happens second can fail, and then the two disagree permanently — and no amount of retrying fixes the case where the process dies in between. The outbox pattern is the standard answer: write the event to a table in the same transaction, and let a separate process publish it.
Conflict resolutionWhen two replicas accept conflicting writes, something must decide. Last-write-wins is simple and silently discards data. Version vectors detect the conflict and hand it back to you, which is honest but needs a policy. CRDTs are designed so that concurrent updates merge deterministically — counters, sets, and the text types that make collaborative editors work — at the cost of only supporting operations that commute.
Topic · 21

The problem catalogue, and what each one is really testing

The list of questions asked is short and stable. Each one exists to force a specific decision — recognise the decision and the rest of the answer assembles itself.

ProblemThe decision it exists to force
URL shortenerKey generation without coordination, and a read path that is almost pure cache.
Pastebin / file shareWhere the bytes live versus where the metadata lives, and signed URLs.
Rate limiterWhich algorithm, and where the counter lives when there are fifty API servers.
News feed / timelineFan-out on write against fan-out on read, and the celebrity who breaks whichever you chose.
Chat / messagingConnection state, delivery receipts, ordering per conversation, and offline devices.
Notification serviceFan-out, deduplication, per-channel retries, and user preferences as a filter.
Typeahead / autocompletePrecomputation, a trie or an index, and a latency budget of tens of milliseconds.
Web crawlerPoliteness, frontier scheduling, deduplication at enormous scale, and traps.
Search engineInverted index, ranking, sharding by document, and index freshness.
Video streamingTranscoding pipeline, adaptive bitrate, CDN economics, and cold storage of originals.
Cloud drive / syncChunking, deduplication, conflict resolution, and delta sync over a bad network.
Ride hailingGeospatial indexing, matching under contention, and location updates as a write flood.
Food deliveryThree-sided state machine, and the fact that the real world can reject your transaction.
Ticket bookingOverselling. Holds, expiry, and a transaction boundary you can defend.
Payments / ledgerDouble-entry, idempotency, exactly-once as an application property, and reconciliation.
Digital walletBalance as a derived value, not a mutable field. Auditability over convenience.
Ad click aggregationStreaming aggregation, late events, watermarks, and approximate versus exact counts.
Metrics / monitoringTime-series storage, cardinality, downsampling, and query cost at read time.
Log aggregationIngest volume, retention tiers, and search over data you cannot afford to index fully.
Distributed cachePlacement, consistent hashing, eviction, and what happens when a node leaves.
Key–value storeReplication, quorums, conflict resolution, and the storage engine underneath.
Object storeDurability arithmetic, erasure coding, and metadata as the real scaling problem.
Job schedulerExactly-once triggering, clock skew, long-running work, and failure mid-job.
LeaderboardSorted sets, approximate ranks at scale, and time-windowed resets.
Collaborative editorOperational transformation or CRDTs, and presence as a separate problem.
Stock exchangeA single-threaded matching engine, determinism, and why sharding is the wrong instinct here.
Proximity searchGeohashing or quadtrees, and the query that crosses a cell boundary.
Recommendation feedOffline candidate generation, online ranking, and a latency budget split between them.
A/B testing platformDeterministic bucketing, exposure logging, and not letting analysis change the assignment.
Feature flagsPropagation delay, fail-open defaults, and a service that must never be down.

Eight of these are worked out in full

The repository this page belongs to carries long-form written solutions to the news feed, URL shortener, chat, typeahead, cloud drive, ride hailing, rate limiter and video streaming problems. They are Markdown, read on GitHub rather than served here.

Part six

Design in the small

The other design round: no traffic estimates, no datacentres, just a domain and forty-five minutes to turn it into types. It rewards different things from the system round — naming, boundaries, and the discipline to leave things out — and it is where object-oriented principles stop being trivia and start being usable.

Topic · 22

From three sentences to a set of types

The exercise has a mechanical opening: extract the nouns, extract the verbs, decide which nouns are types and which are fields, then show one interaction end to end.

THE OPENING FIVE MINUTES OF ANY LOW-LEVEL DESIGN ROUND 1 the sentences 2 nouns and verbs 3 types and interfaces 4 one interaction A car park has several levels. Each level has spots of a few sizes. A vehicle takes a ticket on entry and pays on exit, priced by duration. nouns: ParkingLot, Level, Spot, Vehicle, Ticket, Payment verbs: park, unpark, price, pay SpotSize is an enum, not three subclasses PricingPolicy: interface SpotRepository: interface ParkingLot: the front door park(vehicle): find a free spot claim it atomically issue a ticket return it, or Full The decision that carries the most weight is at stage three: which nouns become types, which become fields, and which become enums. A subclass per spot size is the classic wrong turn: the sizes differ in a value, not in behaviour, so inheritance buys nothing and costs a class each. Say what you are leaving out, too. “No reservations, no season tickets, single site” is scope control, and it is scored.
Stage four is what separates a class diagram from a design. Anyone can list six classes; tracing one call through them exposes whether the boundaries work — who holds the lock, who validates, what is returned when the lot is full, and whether the caller has to know anything it should not. Do the trace even if nobody asks, because it is where the missing interface always shows up.

Concurrency belongs in the design, not after it. “Two cars arrive at the same instant” is asked in almost every one of these rounds, and the answer is not a synchronized keyword sprinkled on a method: it is a single atomic claim on a spot, with the search allowed to be racy because a failed claim just retries.

// ParkingLot.java — the interesting three lines of a low-level design round.
public final class ParkingLot {
    private final SpotRepository spots;      // interface: in memory now, a table later
    private final PricingPolicy pricing;     // interface: hourly now, dynamic later
    private final Clock clock;               // injected, so the tests do not sleep

    public Ticket park(Vehicle vehicle) {
        for (Spot candidate : spots.freeSpotsFor(vehicle.size())) {
            // The search may be stale; the claim must not be. One atomic
            // compare-and-set decides the winner, and the loser simply
            // takes the next candidate -- no lock spans the search.
            if (candidate.claim(vehicle)) {
                return new Ticket(candidate.id(), vehicle.plate(), clock.instant());
            }
        }
        throw new LotFullException(vehicle.size());
    }
}
Topic · 23

SOLID, stated as five failure modes

Reciting the five names scores nothing. Naming the smell each one describes, and the cost of ignoring it, is what the question is for.

EACH PRINCIPLE IS A SMELL PLUS A BILL S one reason to change SMELL a class edited for two unrelated reasons BILL every change risks the other reason O open to extension SMELL a switch on type that grows every release BILL adding a case edits code that worked L subtypes substitute SMELL an override that throws or narrows BILL callers must know the real subclass I small interfaces SMELL implementers stubbing out half the methods BILL one new method breaks everyone D depend on abstractions SMELL a service that builds its own DB client BILL no test without the real database Four of the five are the same instinct in different clothes: keep the reasons for change apart. The fifth, L, is about honouring a promise you made in a type. All five are guidance, not law. A round where you apply every one of them to a forty-line problem reads as worse judgement, not better.
The most useful of the five in an interview is D. Depending on an interface rather than a concrete client is what makes the design testable, and testability is the property interviewers can check in the room — “how would you test this without a database?” is the follow-up in nearly every low-level design round, and it has no good answer if the class constructs its own dependencies.

The neighbouring vocabulary

encapsulationabstractioninheritancepolymorphismcomposition over inheritancecouplingcohesionlaw of DemeterDRYYAGNIKISSseparation of concernstell, don’t askvalue objectentityaggregate rootdomain modelanaemic modelubiquitous languagebounded contexthexagonal architectureports and adaptersclean architecturelayeringdependency injectioninversion of controlimmutabilitydefensive copyingequals and hashCodenull objectfail fastdesign by contractinvariantstate machine
Topic · 24

Design patterns, filed by the problem they solve

Learn them as answers to questions, not as a list of twenty-three names. In an interview you will use six of them, and you should be able to name the trade-off of each.

Strategy“The rule varies.” An interface plus one implementation per rule, chosen at construction. Pricing policies, retry policies, ranking functions, storage backends. The single most useful pattern in a low-level design round, because it is what turns a growing switch into a set of small testable classes — and because it makes the seam the interviewer will ask you to test against.
Factory and abstract factory“Construction is itself a decision.” Keeps new out of the code that uses the thing, so the choice can depend on configuration, on the environment, or on the input. Abstract factory adds a second dimension: a family of related objects that must be consistent with each other. In a language with first-class functions this is often a function, not a class hierarchy, and saying so is a good sign.
Builder“Too many constructor arguments, most of them optional.” Also the standard way to construct an immutable object with validation at the end — the builder is mutable, the product is not, and build() is where the invariants get checked. Beats a telescoping set of constructors and beats a mutable object with fifteen setters, which can be observed half-configured.
Singleton“Exactly one.” Ask whether you mean one instance or one lifetime, because dependency injection with a single registered instance gives you the second without the global state, and the global state is what makes tests interfere with each other. If you do implement one, know how your language makes lazy initialisation thread-safe.
Prototype“Copy this already-configured object.” Rare on its own, but the question it drags in is not: deep copy versus shallow copy, and what your language does by default with nested references. A “copy” that shares a mutable list is the bug.
Adapter“Their interface, my shape.” The boundary object that keeps a third-party type out of your domain, so replacing the vendor is a class rather than a migration. In hexagonal terms it is the adapter behind a port, and it is the concrete reason people say “do not let the SDK type into your core”.
Facade“One door into a subsystem.” What a service class usually is: a small surface that hides four collaborators and a transaction boundary. It is only a facade if it genuinely simplifies — a class that forwards every method one-to-one is a tax with a pattern name.
Decorator“Add behaviour without subclassing.” One interface, wrappers that each add a concern: retries, caching, metrics, logging, circuit breaking. Composable in any order, which is both the strength and the trap — caching outside retry and caching inside retry behave differently, and a design round is a good place to say which you want.
Proxy“Stand in for it.” Lazy loading, access control, remote calls. Worth knowing because your framework probably uses one on your behalf: Spring wraps beans in proxies, which is exactly why an internal call bypasses @Transactional, and why that surprises everyone once.
Composite“A thing and a group of things behave the same.” File systems, menus, permission trees, invoice line items containing sub-items. It makes recursive operations trivial and it makes cycles catastrophic, so if the structure comes from user input, guard against them.
Bridge“Two independent dimensions of variation.” Three shapes and four renderers is twelve classes by inheritance and seven by composition. Reach for it when you notice the class names have started containing two nouns joined by “and”.
Flyweight“Millions of nearly identical objects.” Share the immutable part and keep only what differs per instance. String interning and integer caching are the everyday examples your language already does, and glyphs in a text editor are the classic one.
Observer“Tell whoever cares.” Events, listeners, subscriptions. Two things it always brings with it: the memory leak from never unsubscribing (the subject holds a reference to the listener forever), and the question of whether notification is synchronous — because a slow listener on a synchronous bus becomes the publisher’s latency.
Command“Make the action an object.” Once an action is a value it can be queued, retried, logged, undone, and sent over a wire. It is the pattern behind undo stacks, job queues, and event sourcing, and it is the honest answer to “how would you add undo?” in a text-editor design.
State“Behaviour depends on which mode we are in.” The alternative to an enum plus a growing switch in every method. Model the transitions explicitly and illegal ones become impossible rather than merely unlikely — which is the answer to most vending machine, order lifecycle and connection questions.
Template method“Same skeleton, different steps.” A base class fixes the sequence and subclasses fill in the holes. Easy to overuse: it binds the subclass to the parent’s lifecycle forever, and strategy or a passed-in function usually gives the same flexibility without inheritance.
Chain of responsibility“Try each handler in turn until one takes it.” Middleware, servlet filters, interceptors, and every HTTP framework you have used. The design questions are whether a handler may short-circuit and whether the chain is ordered by configuration or by registration — both of which bite in production.
Iterator“Walk it without exposing how it is stored.” Frequently a coding-round exercise in its own right — implement one over a nested list, or a merged pair of sorted streams — where the real content is lazy evaluation and what happens if the collection changes underneath you.
Mediator“Stop objects wiring themselves directly to each other.” Chat rooms, form fields that enable each other, air traffic control. Turns an n-squared web of references into a hub, and turns the hub into the thing that grows without limit if you are not careful.
Memento“Snapshot and restore without exposing internals.” The state half of undo, where command is the action half. Worth pairing with a note on cost: a full snapshot per step is simple and expensive; a diff is cheap and fiddly.
Visitor“A new operation over a fixed set of types.” Compilers and abstract syntax trees, where the node types are stable and the operations keep multiplying. It trades easily-added operations for hard-to-add types, which is exactly the trade sealed hierarchies and pattern matching make explicit in modern languages.
Interpreter“A tiny language.” Rules engines, query filters, feature-flag expressions, spreadsheet formulas. Turns up disguised as “let users define their own conditions”, and the right first answer is usually a small parsed grammar rather than an ever-growing configuration format.
Repository and unit of workNot in the original book and asked more often than half of it. A repository presents a collection-like interface over storage; a unit of work batches the changes and commits them together. The reason they matter in an interview is testability — they are the seam that lets the service be tested without a database, and the answer to “how would you test this?”
Topic · 25

The low-level design catalogue

A shorter list than the system-design one, and each entry has a known trap. Interviewers reuse the traps far more than they reuse the problems.

ExerciseThe trap
Parking lotSubclassing by spot size. It is an enum and a capacity rule, not a hierarchy.
Elevator systemModelling the car without the scheduler. The interesting object is the dispatcher.
Vending machineMoney and state in the same class. It is a state machine with a coin ledger beside it.
ATMForgetting that dispensing can fail after the balance has changed.
Library / rentalCopies versus titles. Two entities that beginners merge into one.
Chess or tic-tac-toeRules scattered across pieces. A move validator with a board is easier to test and to extend.
Deck of cardsShuffling correctly. Fisher–Yates, and a seeded random for testability.
LRU cacheHash map plus doubly linked list, and the eviction that must be O(1) with the lock held once.
Rate limiterWhere the state lives, and what happens on a clock jump.
In-memory key–value storeExpiry: lazy on read, or a sweeper, and what each costs.
Logging frameworkLevels, appenders and formatters as three separate abstractions, plus asynchronous flushing.
Notification serviceChannels as strategies, preferences as a filter, and retries per channel.
Splitwise / expense sharingBalances as a derived graph, not stored pairwise. Simplifying debts is the follow-up.
Food ordering / cartPricing rules as composable strategies; totals recomputed, never stored mutable.
Ride matchingDriver state transitions, and matching as a service the driver object does not own.
File systemComposite pattern, path resolution, and permissions inherited down the tree.
Text editor / undoCommand objects and a stack. Ask whether redo is in scope before designing for it.
Job schedulerA priority queue plus workers, and what a task retry does to ordering.
Snake / game loopTick-based state updates, and keeping input, state and rendering apart.
Hotel or flight bookingInventory holds with expiry — the same overselling problem as the system-design version, in one process.
Topic · 26

API design

Asked on its own, and as a sub-question of both design rounds. The whole subject is: what is the resource, what can go wrong, and how does the client find out.

Resources and verbsNouns in the path, verbs in the method: POST /links, GET /links/{code}, DELETE /links/{code}. Plural collections, identifiers in the path, filters in the query string, and nesting only where the child genuinely cannot exist without the parent. When an operation is truly not CRUD — publish, cancel, retry — a sub-resource (POST /orders/{id}/cancellations) beats inventing a verb, and either beats a ?action= parameter.
Status codes as a contract200 with a body, 201 with a Location, 202 for accepted-but-not-done, 204 for nothing to say. 400 you sent nonsense, 401 you are not authenticated, 403 you are but may not, 404 no such thing, 409 conflict with current state, 422 well-formed but semantically wrong, 429 slow down. 5xx means it is our fault — so returning 500 for a validation failure tells every client to retry something that will never succeed.
IdempotencyPUT and DELETE are idempotent by definition; POST is not, and is made so with a client-supplied Idempotency-Key header, a unique index, and a stored response. Say which of your endpoints are safe to retry, because that is what every client library and every proxy in the path needs to know before it retries on your behalf.
PaginationOffset is simple and wrong at scale twice over: the database counts and discards the skipped rows, so deep pages get slower, and a row inserted during paging shifts everything so the client sees a duplicate or misses one. Cursor pagination encodes the sort key of the last row — opaque, stable under insertion, and O(1) to resume. Always cap the page size server-side.
Filtering and sortingWhitelist the fields. An open sort= parameter is an unindexed query waiting to be discovered, and an open filter language is a denial-of-service tool your users did not know they had. Document which fields are filterable, back each with an index, and reject the rest with a 400 rather than silently ignoring it.
ErrorsA stable machine-readable code the client can branch on, a human-readable message that is not that code in a sentence, the offending field where relevant, and a request_id that ties the response to your logs. Never a stack trace and never a raw database error — both leak your schema. RFC 9457 “problem details” is a reasonable off-the-shelf shape.
VersioningA version in the URL is ugly and unambiguous; a header is clean and gets lost by proxies and caches. The easy half is choosing; the hard half is retirement — how a field is deprecated, who you tell, how you find out which clients still use it (log it), and how long you carry both. A version you can never remove is a permanent tax.
CompatibilityAdditive changes only: new optional fields, new endpoints, new enum values only if clients were told to tolerate unknown ones. Never repurpose a field, never tighten validation without warning, never change a default. What breaks clients is rarely what you expected — someone is parsing your error string, and someone else depends on the order of your array.
Rate limitsCommunicated, not just enforced: 429 with Retry-After, plus headers for the limit, the remainder and the reset. A limit a client cannot see is a limit it will hammer, retry, and amplify. Decide the dimension too — per API key, per user, per IP, per endpoint — because a global limit lets one noisy tenant starve everyone.
gRPCSchema-first with generated clients, binary framing over HTTP/2, four call shapes including bidirectional streaming, and deadlines that propagate across hops so one timeout is honoured end to end. The costs are real: no browser without a proxy, no curl, and a schema registry to run. Excellent between your own services, awkward as a public API.
GraphQLThe client picks the shape, which removes the over-fetching argument and hands you three new problems: query cost control (a nested query can ask for the world), caching (every query is a POST to one URL), and the N+1 problem moving server-side, which is what dataloaders exist to batch away. Good fit for many diverse clients, poor fit for a small internal API.
WebhooksPush to a URL the customer controls, which means the failure modes are theirs. Sign the payload with an HMAC and a timestamp so they can verify it and reject replays; retry with backoff for a bounded time; make the payload idempotent with an event id; and give them a way to replay history, because at some point their endpoint was down for a day and they will ask.
Long-running work202 Accepted plus a status resource they can poll, or a callback when it is done. Never hold a connection open for four minutes: proxies, load balancers and mobile networks will all cut it at some point you do not control, and the client cannot tell a lost connection from a failed job. Return the job id in the first response so it is addressable from the start.
Bulk operationsThe design problem is partial success. One status code for a hundred items cannot describe “ninety-seven worked”, so return a per-item result array with the index, the outcome and the error, and use 207 or a 200 with a documented envelope. Then decide whether the batch is atomic — and say so, because callers will assume whichever is worse for them.
// errors.json — an error body worth defending in an interview. Three
// properties matter:
// a code the client can branch on, a message a human can read, and an id
// that ties this response to the log line that explains it.
{
  "error": {
    "code": "link_code_taken",
    "message": "That short code is already in use.",
    "field": "code",
    "request_id": "01JB8Z0X4K9M2QW7N6R3T5V8Y1",
    "retryable": false
  }
}

// Cursor pagination, because offset pagination shifts under inserts:
// GET /v1/links?limit=50&cursor=eyJpZCI6IjAxSkI4WiJ9
{
  "items": [],
  "next_cursor": "eyJpZCI6IjAxSkI5QSJ9"
}
Part seven

Languages and runtimes

Two rounds’ worth of questions hide here: the ones asked of any language, which test whether you understand the machine underneath the syntax, and the ones asked of yours, which test whether you have read the manual or only the tutorial.

Topic · 27

The five questions asked of every language

Whatever you claim on your CV, expect these five. They are the same five for Java, Python, Go, C++, TypeScript and Rust; only the answers change.

THE SAME FOUR QUESTIONS, EIGHT ANSWERS MEMORY CONCURRENCY ERRORS TYPES Java GC, generational threads, now virtual ones checked and unchecked static, erased generics Python refcount plus GC GIL, asyncio, processes exceptions, duck typed dynamic, optional hints Go GC, low pause goroutines and channels errors are values static, structural C++ manual, RAII threads and atomics exceptions or codes static, templates TypeScript GC in the engine event loop, promises throw, and rejections structural, erased Rust ownership, no GC threads, Send and Sync Result and Option static, traits, lifetimes C# GC, generational tasks, async and await exceptions static, reified generics Kotlin GC, on the JVM coroutines exceptions, none checked static, null aware The fifth question does not fit a column: how do you build and ship it? Dependency resolution, lock files, reproducibility, the artifact you deploy. Answer for the language you claimed, in one sentence each. Hesitating on your own language’s memory model is the most expensive kind of gap.
The interesting follow-up is always the same: what does that cost? Garbage collection costs pauses and headroom; ownership costs compile time and a learning curve; a global interpreter lock costs parallelism but simplifies the C API; checked exceptions cost ceremony and buy an explicit contract. A candidate who can name the price of their own language’s design is demonstrating exactly the judgement the design rounds are also testing.
MemoryWhere a value lives (stack or heap, and what decides), what a reference actually is, and how memory comes back. For a garbage-collected language: which collector, what a pause costs, what generational means, and why a leak is still possible — a cache with no eviction, a listener never unsubscribed, a thread-local never cleared. For a manual one: ownership, RAII, and who frees what. The interesting follow-up is always the same: what does that cost?
ConcurrencyWhat a unit of concurrency costs (an OS thread is roughly a megabyte of stack; a goroutine or virtual thread is kilobytes), what is shared by default, what the language guarantees about visibility, and how cancellation propagates. “How many can I have” and “what happens when one panics” are the two questions that separate reading about it from using it.
ErrorsValues or exceptions; checked or unchecked; and what happens to one nobody handles. The trade is explicitness against ceremony: Go returns errors so every call site shows the branch and the code is noisier; exceptions keep the happy path clean and let a failure travel silently through five frames. Whichever you have, know how to add context without losing the original, and what your language does to an error thrown while unwinding.
The type systemStatic or dynamic, nominal or structural, and how much survives to runtime — Java erases its generics, C# reifies them, TypeScript erases everything. Then variance: why a list of cats is not a list of animals if you can add to it. Generics questions are really asking whether you understand the boundary between what the compiler proves and what it merely permits.
Build and dependenciesHow a version is resolved, whether a lock file pins the whole tree, how a transitive conflict is settled, and what artifact actually ships. Reproducibility is the question underneath: can someone check out last year’s tag and build the same bytes? The answer involves lock files, pinned base images, and vendoring or a proxy so that a deleted upstream package cannot break your build.
Equality and hashingValue equality versus identity, and the contract that equal objects must hash equally — break it and a hash map loses your key while still containing it. Every language has this contract and most candidates have never read theirs. The related trap: mutating an object after using it as a map key.
Mutability and copyingShallow versus deep copy, and where the boundary sits in your language — a copied struct containing a slice or a list shares the underlying storage in most of them. Defensive copies at API boundaries, immutable value types where you can, and the mutable-default-argument bug that exists in more languages than Python.
The standard collectionsWhich one to reach for and what it costs: the growth policy of a dynamic array, what happens when a hash map resizes, whether iteration order is defined (and whether relying on it is a bug), and which operations are amortised rather than guaranteed. Interviewers do ask what happens on rehash, because it is the difference between using a library and understanding one.
The performance toolsThe profiler, the allocation profile, the race detector, and the flag that dumps what the compiler decided. Have one story about using them on real code — what you suspected, what the profile actually said, and how much the fix was worth. That story is worth more than any amount of theory about the runtime.
IdiomWhat a reviewer in that language would object to. Writing Java in Python, or Python in Go, reads as inexperience even when the code is correct, and it is visible in the first ten lines. Idiom is also where the language’s safety features live: context propagation in Go, resource blocks in Python and Java, borrow rules in Rust.
Topic · 28

The questions asked of each language

Pick the row for the language on your CV and be able to answer all five without preparation. These are the ones that come up again and again.

LanguageWhat gets asked
JavaJVM memory areas and the collectors; equals/hashCode; generics and type erasure; final, immutability and records; the collections and their complexities; the memory model and volatile; virtual threads against the old pool; streams and when they hurt; try-with-resources; classloading; Spring’s proxies, scopes and transaction boundaries.
PythonNames against variables and the object model; mutable default arguments; the GIL and what it does and does not prevent; generators and laziness; decorators and closures; context managers; __slots__, dataclasses and memory; MRO and multiple inheritance; type hints and what they do not check; packaging, virtual environments and the import system.
GoWhat a slice really is and how append aliases; maps and iteration order; interfaces and the nil-interface trap; goroutine leaks and context; channels, select and closing; defer ordering and evaluation; escape analysis; error wrapping and errors.Is/As; the race detector; modules and vendoring.
C++RAII and ownership; the rule of five; move semantics; smart pointers and cycles; virtual dispatch and the vtable; templates and instantiation; undefined behaviour; the memory model and atomics; copy elision; the standard containers and iterator invalidation.
JavaScript / TypeScriptThe event loop, microtasks and macrotasks; closures and this; prototypes; promises, async/await and error propagation; modules; structural typing and any; narrowing and discriminated unions; what the compiler erases; bundling and tree shaking.
RustOwnership, borrowing and lifetimes; Result, Option and the ? operator; traits and dynamic dispatch; Send and Sync; interior mutability; async and the executor; what unsafe actually permits; cargo and features.
C#The CLR and its collector; value versus reference types and boxing; async/await and the synchronisation context; LINQ and deferred execution; IDisposable; reified generics; nullable reference types; records and spans.
KotlinNull safety and platform types; coroutines, scopes and structured concurrency; data classes; extension functions; sealed classes and exhaustive when; interoperability with Java.

Three of these have a page each

The Python, Java and Go rows above are each expanded into a full page in this series: Python, End to End, Java, Then Spring and Reading Go, all in the rail below. Each ends with the same production service built in that language, which is the fastest way to see what “idiomatic” means in practice.

Part eight

Craft: testing, debugging, review, refactoring

The rounds that look at how you work rather than what you know. They are increasingly common because they are harder to prepare for by memorisation — a debugging round with a real repository and a failing test tells an interviewer more in twenty minutes than an algorithm question tells them in forty-five.

Topic · 29

Testing, and what each layer is actually for

“How would you test this?” follows almost every coding answer. The good response names layers and categories, not examples.

FOUR LAYERS, AND WHAT EACH ONE IS THE ONLY WAY TO CATCH FEWER · SLOWER · BROADER end to end Catches wiring, config and deploys. Costs minutes and flakiness. contract Catches two teams disagreeing about a payload, before production does. integration Catches SQL, serialisation, migrations and the framework’s defaults. unit Catches logic and edge cases. Runs in milliseconds, so it runs often. MORE · FASTER · NARROWER The shape is a consequence, not a goal: you write more of the tests that are cheap to run and quick to diagnose.
Containers moved the line between unit and integration. Spinning a real database up for a test suite now costs seconds, so “the unit test mocks the repository” is a weaker answer than it was — a test against the real engine catches the SQL a mock cannot. The honest position: mock what you do not own and cannot run; use the real thing for everything else.
UnitOne behaviour, no I/O, named after the behaviour rather than the method — rejects_a_code_that_is_already_taken, not testCreate2. If the name needs an “and”, it is two tests. Arrange, act, assert, with one logical assertion; a test with six unrelated assertions fails once and tells you about one of them.
IntegrationYour code plus something real: a database in a container, an HTTP server, a message broker. This is where most production bugs actually live — the SQL a mock cannot check, the serialisation of a timestamp, the framework default nobody set. Containers made these cheap enough that “we mock the repository” is now a weaker answer than it was five years ago.
ContractThe consumer writes down what it needs from the provider; the provider runs those expectations in its own build. It catches the mismatch that integration tests miss because each side was tested against its own idea of the payload. Worth naming in any microservices design round, because the alternative is finding out in staging.
End to endA real user journey through the deployed system. Few, slow, and treated as a smoke test rather than a safety net — a suite of four hundred of them is a suite nobody trusts, because the failures are usually the environment. The honest position: enough to prove the pieces are wired together, and no more.
Property-basedAssert a law that must hold for any input and let the framework generate thousands: round-tripping an encode and decode returns the original, the output of a sort is a permutation of the input and is ordered, two ways of computing a total agree. When it fails it shrinks the case to the smallest one that still breaks, which is often a bug you would never have thought to write down.
FuzzingRandom and mutated input against anything that parses bytes — a decoder, a header parser, a file format. Startlingly effective for its cost, because the failure mode being hunted is a crash or a hang rather than a wrong answer, so no oracle is needed. Modern toolchains ship it: Go, Rust and libFuzzer all make it a few lines.
Golden and snapshot testsCompare output against a recorded file. Useful for rendered output and generated code, and dangerous in exactly one way: when the update is a single keystroke, a wrong snapshot gets blessed without being read. Keep them small, keep them reviewable, and never regenerate a whole directory to make the build green.
Load and soakLoad tests find the throughput knee and the latency percentiles under it; soak tests run for hours and find what leaks — memory, file descriptors, connections, disk. Nothing else finds a slow leak, and nothing else tells you what your service does at 80% utilisation, which is where queueing starts to dominate.
ChaosInject the failure you claim to survive, deliberately, in a bounded window, with a way to stop. Kill an instance, add latency to a dependency, fill a disk. The point is not the failure but the discovery that the retry storm you designed makes the outage worse, or that a health check was checking the wrong thing.
The five test doublesDummy is passed and never used. Stub returns canned answers. Spy is a stub that records what it was called with. Mock is pre-programmed with the calls it expects and fails if they do not happen. Fake is a real, working, simplified implementation — an in-memory repository. The distinction interviewers actually probe: a stub verifies state, a mock verifies interaction, and over-mocking couples the test to the implementation so tightly that refactoring breaks it.
Fixtures and factoriesA builder that produces a valid object with sensible defaults and lets the test override the one field it cares about beats a shared fixture that eleven tests depend on differently. The test should say, in its own body, what makes this case interesting — if you have to scroll to a setup file to find out, the test is not readable.
DeterminismFlakiness is almost always one of three things: time, randomness, or ordering. Inject the clock so “expires in an hour” is testable without sleeping; seed the generator and print the seed; and never depend on map iteration order or on two goroutines finishing in a particular sequence. A test that sleeps to wait for something is a test that fails on a loaded build machine.
Flaky testsA flaky test is worse than no test, because it teaches the team to re-run the build. Quarantine it immediately so it stops blocking, then fix or delete it on a deadline — a quarantine with no expiry is a graveyard. Retrying on failure to make the pipeline green is how a real intermittent bug hides for a year.
CoverageA tool for finding untested branches, not a target. Say that explicitly, because it is a values question: a team with a mandated 80% writes tests for getters, and a mutation-testing tool would show that a good fraction of the covered lines are not actually asserted on. Branch coverage on the module that handles money is worth more than a number for the repository.
TDDRed, green, refactor — and the actual benefit is design pressure: code that is hard to test is usually code with the dependencies pointing the wrong way. Have an opinion, including where it does not fit (exploratory work, code whose shape you do not know yet), because “always” and “never” both read as inexperience.
Pyramid, trophy, or honeycombThe classic pyramid puts the weight on unit tests because they were the only fast ones. Front-end practice moved the weight up to integration, because rendering a component with its children is what actually catches bugs; service practice did the same once containers made real dependencies cheap. Know the shape you argue for and why the economics of your stack put it there.
Topic · 30

The debugging round

A repository you have never seen, a failing test or a broken endpoint, and someone watching how you narrow it down. Method is the whole score; finding the bug quickly with no visible method scores worse than finding it slowly with one.

  1. Reproduce it first. A bug you cannot reproduce is a bug you cannot confirm you fixed. Get it failing on demand before you change anything.
  2. Read the error. All of it, including the frames in the middle. An astonishing number of debugging rounds are won here and lost by skimming.
  3. Say what you expect before you look. “If the parser is at fault, this value will be empty here.” Now the observation means something either way.
  4. Bisect. In the code path, in the input, or in history with git bisect. Halving the search space beats inspecting it.
  5. Change one thing at a time, and put it back if it was not the cause. A pile of speculative edits destroys the evidence.
  6. Find the cause, not the symptom. Then say what class of bug it was and what would have caught it — that sentence is the senior signal in this round.
The toolsA debugger with breakpoints, conditional breakpoints and watch expressions — not only print statements, and being seen to reach for one is part of the grade. Plus git bisect to find the commit, git blame to find the reason, and a profiler when the symptom is “slow” rather than “wrong”.
Reading a stack traceAll of it, including the middle frames everybody skips. Where it was thrown, where it was caught, and what “caused by” means — the innermost cause is usually the real one and the outermost is your framework’s opinion about it. An astonishing number of debugging rounds are won by reading the whole message aloud.
Logs and correlationOne request id threaded through every service and every line, so a single grep reconstructs the journey. Then the ability to raise the level on one component without drowning in everything else. If the answer to “what happened to this request” requires joining timestamps by eye, that is the finding.
Debugging in productionWhere you cannot attach a debugger: heap dumps for what is retaining memory, thread dumps for where everything is stuck, a flame graph for where the CPU went, strace for what it is asking the kernel, packet capture when the story stops making sense. Continuous profiling makes several of these retrospective rather than reactive.
HeisenbugsTiming, ordering, uninitialised memory, and the log line that makes it disappear — because the log call added a memory barrier, or slowed the loop enough to lose the race. When observation changes behaviour, stop observing and start reasoning: what would have to be true for this to happen at all?
BisectingThe fastest route when it worked last week and nobody knows what changed. git bisect run ./repro.sh turns twenty commits into five automated steps. It works on inputs too: halve the payload until the failure disappears, and on configuration: revert half the settings.
The three usual suspectsConfiguration, data, and the last deploy. Check them before suspecting the algorithm, because they are responsible for most incidents and all three are cheap to rule out: what changed in the last hour, is this input unlike the others, and is the config in this environment the config you think it is.
Knowing when to stopTime-box it, then say what you know and ask. “I have ruled out the parser and the transport; I would look at the encoding next, but I would like a hint on where you would look” is a scoreable sentence and stubborn silence is not. Interviewers score the escalation, because that is what you will do on the team.
Topic · 31

Code review, as a round and as a skill

You are handed a pull request and asked what you think. The trap is spending ten minutes on naming and never mentioning the missing transaction.

Review in a fixed order and say the order out loud, because it demonstrates a system rather than a set of reactions: correctness, then security and data safety, then failure behaviour, then tests, then design and naming, then style. Style last, and only where a tool cannot enforce it. Distinguish blocking comments from suggestions explicitly — a reviewer who marks everything as equally important is a reviewer nobody can act on.

Correctness firstDoes it do what the description claims? Walk the unhappy path before the happy one: empty collection, null, the error branch, the early return that skips the cleanup. Most review comments are about style because style is easy to see; the valuable ones are about the case the author did not consider.
ConcurrencyShared mutable state reachable from two requests, a check-then-act that should be one atomic operation, a lock held across I/O, an unbounded queue, a cache without a size limit. These are the findings that never show up in testing and always show up at peak.
Data safetyA migration that takes a lock on a large table, a DELETE or UPDATE whose WHERE could match everything, a backfill with no batching, a write with no transaction where two writes must agree. Ask how it is reversed — and if the answer is “restore from backup”, that is a blocking comment.
SecurityA query built by concatenation, a missing object-level authorisation check, a secret in the diff, unvalidated input reaching a shell or a file path, a token or a password written to a log. Also new dependencies: who publishes it and how many transitive packages did it bring.
Failure behaviourIs there a timeout on every outbound call, is the retry safe to repeat, what happens on partial failure, and does the error that reaches the user say anything useful? A missing timeout is the single most common cause of a small outage becoming a large one, and it is invisible unless you look for it.
TestsNot “are there tests” but “would these fail if the code were wrong?” Look for a test that asserts on the interesting case rather than the trivial one, and be suspicious of a test that mirrors the implementation line for line — it will break on every refactor and catch nothing.
ObservabilityCan you tell from outside whether this works? A new code path with no metric and no log is a path you will debug blind at three in the morning. Conversely, a log line inside a hot loop is a cost; ask what the volume will be.
Design and duplicationIs the boundary in the right place, is this the third copy of something that wants extracting, does this class now do two things? Say it once, as a suggestion, with a reason — and be willing to let it go, because a review is not the place to redesign work that is already written.
ReadabilityNames that match the domain, functions short enough to hold in your head, and comments that say why rather than restating what. Anything a formatter or a linter can decide should not be a human comment at all; if it keeps coming up, the fix is a tool, not a reviewer.
Tone, and what is blockingAsk questions rather than issue verdicts, say what is good as well as what is wrong, and mark clearly which comments block and which are optional — a reviewer who flags everything at the same weight is one nobody can act on. This is scored: the round is partly about whether people will want to send you their code.
Topic · 32

Refactoring, legacy code and the take-home

“Here is some working but unpleasant code — improve it.” The failure mode is rewriting it. The pass is small, safe, reversible steps with the tests green after every one.

Characterisation testsBefore changing legacy code, pin its current behaviour — bugs included — by writing tests that assert what it does today. You are not testing correctness, you are building a net. This is the entire technique, and saying it is the difference between refactoring and rewriting-and-hoping.
SeamsA place where you can substitute a dependency without editing the logic: a constructor argument, an interface, an injected clock, a factory method you can override. Finding the seam is the whole skill in legacy work; once it exists, the untestable class becomes testable in one commit and everything else follows.
The catalogueExtract function, extract class, inline, rename, move, introduce parameter object, replace conditional with polymorphism, replace magic number with a constant. Small, named, mechanical, and individually reversible — and every editor automates half of them. The discipline is doing one at a time with the tests green in between, not doing them all in one commit.
Strangler figPut a facade in front of the old system and route slices of traffic to a new implementation, one capability at a time, until nothing reaches the old one and it can be deleted. It is how you replace a system you are not allowed to stop, and it is the answer to “how would you migrate off the monolith” that does not involve a two-year branch.
Branch by abstractionThe in-process version: introduce an abstraction over the thing you want to replace, implement the new one behind it, switch with a flag, remove the old. Everything stays on the main branch and every commit ships, which is why it beats a long-lived refactor branch that will not merge.
Code smellsLong method, large class, long parameter list, feature envy (a method more interested in another object’s data), shotgun surgery (one change touches nine files), primitive obsession (a string that is really an identifier), and duplicated logic that has already drifted. Name the smell and the refactoring that answers it; that pairing is what the question is checking.
Technical debtDeliberate debt is a decision with a repayment plan; accidental debt is just a mess. Being able to argue for paying some down in business terms — this costs us two days a month, this is why the last three incidents happened here — is a senior signal, because it is the form the argument has to take to win.
Knowing when not toCode that is ugly, correct, and never touched is not a priority. Refactoring it costs review time and adds risk for no return. Saying so is judgement, not laziness, and it is the answer that distinguishes someone who has maintained a large system from someone who has read about one.

The take-home is graded on the parts you might skip

Reviewers look for a README that says how to run it and what you traded away, a commit history that shows the order you worked in, tests that fail when the code is wrong, and error handling on the unhappy path. Scope it to the time given and write down what you would do with more — an unfinished feature you named beats a finished one that was never mentioned. Then expect the follow-up conversation to ask why every interesting line is the way it is; that conversation, not the code, is the round.

Part nine

Shipping it and running it

Everything between a commit and a user. These questions have grown from a niche for infrastructure roles into a standard part of every backend loop, because the industry decided that writing the code is roughly half of the job.

Topic · 33

Version control and continuous integration

Git questions are practical, not theoretical: they want to hear that you have recovered from a mistake, not that you can recite the object model — though the object model is what makes the recovery obvious.

The object modelBlobs are content, trees are directories, commits point at a tree and at parents, branches and tags are just pointers to commits, and HEAD points at a branch. Once you see that a branch is a movable label, most git questions answer themselves — including why a rebase creates new commits, why a force push loses someone else’s work, and why nothing is really deleted until garbage collection runs.
Merge versus rebaseMerge preserves what actually happened and leaves a commit with two parents; rebase rewrites your commits onto a new base for a linear history that is easier to read and to bisect. The rule that matters: never rebase commits other people have pulled, because you are handing them a different history with the same branch name. Squash-on-merge is the common compromise.
Cherry-pick, revert, resetcherry-pick copies a commit elsewhere. revert creates a new commit undoing an old one and is the safe choice on a shared branch. reset moves the branch pointer: --soft keeps everything staged, --mixed keeps the files, --hard discards your working tree — and that last one is the only command here that loses uncommitted work.
Recoveringgit reflog lists every position HEAD has held, including the ones a reset or a bad rebase abandoned, so almost nothing that was ever committed is genuinely lost for about ninety days. Knowing this is the difference between a bad afternoon and a lost day, and it is a fair interview question precisely because it turns panic into a two-line fix.
Conflict resolutionUnderstand both sides before choosing either — git log --merge shows the commits that touched the region. Lock files and generated files are the special case: never hand-merge them, take one side and regenerate. And a conflict that keeps recurring on the same file is telling you the branch has been open too long.
Branching strategiesTrunk-based with short-lived branches and feature flags, GitHub flow with a branch per pull request, or git flow with release and develop branches. The variable that actually predicts pain is branch lifetime: merge cost grows superlinearly with how long a branch has been diverged, which is the argument for merging daily rather than for any particular diagram.
Pull requestsSmall enough to review properly — past roughly four hundred lines, review quality collapses and you get a rubber stamp. A description that says why, not just what. Stacked pull requests for a large change, so each piece is reviewable. And a green build before a human is asked to look.
Monorepo versus polyrepoA monorepo gives atomic cross-project changes, one version of everything, and easy refactoring across boundaries; it costs you build tooling that understands the graph, and CI that only runs what changed. Polyrepo gives independent release cycles and simple tooling; it costs you dependency version drift and change coordination. Both work; the question is which cost your organisation can afford.
What CI must doRun on every commit, fast enough that people wait for it (ten minutes is the usual threshold before they stop), and be trustworthy — a pipeline with known-flaky tests is a pipeline nobody reads. Build once and promote the same artifact through environments, rather than rebuilding per environment and shipping something you never tested.
Reproducible buildsLock files committed, base images pinned by digest rather than by tag, build tools versioned, and no network fetch of a floating dependency at build time. The test is whether checking out last year’s tag produces the same artifact — and the reason it matters is that a security investigation starts with “what was actually in that build”.
Caching in CIDependency caches and layer caches are what keep a pipeline under ten minutes. The correctness risk is a stale cache producing a build that cannot be reproduced from a clean checkout, so key the cache on a hash of the lock file and let it miss when that changes. A weekly clean build is a cheap way to find out you have been depending on the cache.
Static analysisFormatters (no opinions, no discussion), linters, type checkers, dependency vulnerability scanning, and secret scanning on every push. Everything a tool can decide should never be a review comment. Secret scanning in particular belongs in a pre-commit hook as well as in CI, because by the time CI catches it the secret is already in the history and must be rotated.
Topic · 34

Deployment, containers and rollback

The question behind all of them: how does a bad version stop hurting people, and how fast? Every strategy below is a different answer to that.

THREE WAYS TO REPLACE A RUNNING VERSION rolling 4 old 3 old + 1 new 2 old + 2 new 1 old + 3 new 4 new blue–green blue serves all green built green smoke tested switch, all at once blue kept warm canary 1% of traffic watch the metrics 10% 50% 100% Rolling is cheapest and means two versions are live at once — so the database must satisfy both, which is why migrations expand before they contract. Blue–green rolls back in seconds and costs double the capacity. Canary limits the blast radius but needs metrics good enough to decide on. None of the three protects you from a migration that has already dropped a column. That one is not a deployment problem.
The question under the question is schema change. Every strategy here has old and new code live at the same moment, or at least a moment where you might want the old code back. That is the whole argument for expand–migrate–contract: add the column, write both, backfill, switch reads, and only then drop the old one — several deploys apart, so that any single one of them is reversible.
ContainersAn image is layers plus a manifest; the layers are cached by content, so ordering the Dockerfile from least to most volatile (dependencies before source) is what makes rebuilds fast. Multi-stage builds keep the toolchain out of the runtime image. Run as a non-root numeric user, use the exec form of CMD or ENTRYPOINT so signals reach the process rather than a shell, and pin the base image by digest.
OrchestrationDeployments manage replica sets which manage pods; services give them a stable name; probes decide what is alive. The two probes are different questions and conflating them causes restart loops: liveness failing restarts the container, readiness failing only takes it out of the load balancer. A readiness probe that checks the database will take your whole fleet out when the database blips.
Requests and limitsA request is what the scheduler reserves; a limit is what the kernel enforces. Exceed a memory limit and the process is killed (exit 137); exceed a CPU limit and it is throttled, which shows up as latency with no obvious cause. Requests set far below actual usage is how a node gets oversubscribed and everything on it gets slow together.
ConfigurationEnvironment variables and mounted files, separate from the image, so the same artifact is promoted through environments unchanged. Validate the whole configuration at start-up and refuse to boot on a missing value — failing the deploy is much cheaper than failing the tenth request. Secrets come from a secret manager, not from the same place as the rest.
Infrastructure as codeDeclared state, a plan that shows the diff, and drift when someone edits the console by hand. The review of the plan is the actual safety mechanism, which is why a plan that says “will destroy the database” must be impossible to approve accidentally. State files hold secrets and need locking, and a workspace per environment beats copy-pasted directories.
Immutable infrastructureReplace, never patch: a change means a new image and a new instance, so what runs is always what was built and tested. It makes rollback a deploy of a previous artifact rather than an operation, and it removes the entire class of “this machine has been up for two years and nobody knows what is on it”.
MigrationsExpand, migrate, contract, across several deploys, because old and new code are live at the same moment. Backfills in batches with a sleep, so replication lag does not spike. Never take a long lock at peak: adding a column with a volatile default, or an index without CONCURRENTLY, will stop the site on a large table.
Feature flagsThey decouple deploy from release: ship the code dark, turn it on for 1% of users, turn it off in seconds without a build. That last property is the real value, because it is the only rollback that is faster than a deploy. The cost is combinatorial: every flag doubles the states your system can be in, so flags need owners and removal dates.
RollbackA plan, not a hope. Know how long it takes, whether it has been exercised this quarter, and — the question people miss — what is not reversible: a migration that dropped a column, a message already consumed, an email already sent. Design so the irreversible steps are as late and as few as possible.
Secrets managementStored in a manager, injected at runtime, rotated on a schedule, and never in an image layer or a repository — and note that deleting a committed secret does not remove it from the history, so the response is always rotation. Short-lived credentials issued per workload beat long-lived ones you have to protect.
AutoscalingChoose the signal deliberately: CPU is easy and often wrong for an I/O-bound service, where queue depth or in-flight requests track load far better. Then the two things that make it useless in a spike — the metric’s delay and the instance’s cold start. If a new pod takes ninety seconds to serve traffic, autoscaling is a cost-control mechanism, not an availability one.
CostWhat the architecture costs per month and which line dominates — increasingly asked at senior level and rarely prepared for. The usual surprises are not compute: cross-zone and egress data transfer, over-provisioned always-on non-production environments, log and metric retention, and storage nobody deleted. Being able to name a cost you personally reduced is a strong answer.
Topic · 35

Observability and service levels

Three signals, one budget. The senior version of this question is not “what do you log” but “how do you know it is working, and what number tells you when to stop shipping features”.

THREE SIGNALS, EACH ANSWERING A DIFFERENT QUESTION metrics Is it healthy, right now, in aggregate? Cheap, bounded, the only thing you alert on. traces Where did this one request spend its time? Sampled, and the only cross-service view. logs What exactly happened here? Structured, correlated by request id, and expensive. ONE MONTH OF ERROR BUDGET AT A 99.9% OBJECTIVE — 43 MINUTES IN TOTAL one 40-minute incident spends most of the month policy: freeze features here day 1 day 30 The budget is what makes reliability a decision rather than an argument: while there is budget left you ship, and when there is not, you stop.
Notice how little 99.9% is. Three nines allows about forty-three minutes of failure a month, which one bad deploy can spend in a single afternoon; four nines allows four minutes, which is less than most human response times, so it can only be achieved by automation. This arithmetic is why “what availability do you need?” belongs in the requirements phase of a design round — the answer changes the architecture, not just the dashboard.
The four golden signalsLatency, traffic, errors, saturation. A dashboard with those four per service answers “is it healthy” in five seconds, and the discipline of having exactly four stops it becoming a wall of forty graphs nobody reads. Split latency by success and failure — fast errors otherwise flatter your p99.
PercentilesAverages hide everything: an average of 50 ms is consistent with a tenth of users waiting a second. Quote p50, p95, p99, and know that at p99 a page making a hundred backend calls will hit the slow path almost every time — which is why tail latency matters more than it seems. You cannot average percentiles across instances; you need histograms and a quantile computed over the merged buckets.
SLI, SLO, SLAThe indicator is the measurement (proportion of requests under 300 ms). The objective is the internal target you manage to (99.9% of them, over 30 days). The agreement is the external contract with financial penalties, and it is always looser than the objective, so that you find out before the customer does. Three different things and the interview does check.
Error budget100% minus the objective, expressed as time or as requests: 99.9% over thirty days is about 43 minutes. It converts reliability from an argument into a number — while there is budget left you ship features, and when it is spent you stop and fix things. It also makes over-delivery visible: a service that never spends its budget was over-engineered.
Structured loggingFields, not sentences, so a log line is queryable: level, msg, request_id, and the identifiers relevant to that event. One line per meaningful event, not one per function. Never log secrets, tokens, passwords or full request bodies — and be aware that a log statement inside a hot loop is a cost measured in gigabytes a day.
Distributed tracingA trace is a tree of spans across services, joined by a context propagated in headers. It is the only view that answers “where did this one request spend its time” when the answer is three hops away. Sampling is what makes it affordable: head sampling decides up front and is cheap, tail sampling decides after the fact and can keep every slow or failed trace, which is what you actually want.
CardinalityA metric with a label per user id creates one time series per user and will bankrupt your metrics system — this is asked as a trap. Labels must be bounded and low: status code yes, endpoint pattern yes, raw path no, user id never. High-cardinality identifiers belong on logs and traces, which are indexed differently and priced differently.
AlertingAlert on symptoms the user feels — error rate, latency, budget burn — not on causes like CPU, which fire constantly and mean nothing on their own. Every page needs an action; if the response is “acknowledge and go back to sleep”, delete it. Multi-window burn-rate alerts (fast burn pages, slow burn opens a ticket) are the modern shape, and alert fatigue is a real failure mode with a real cost.
Health checksLiveness answers “should I be restarted” and must depend on nothing external. Readiness answers “should I get traffic” and may check the dependencies you cannot serve without. Getting this backwards — a liveness probe that checks the database — turns a dependency blip into a fleet-wide restart storm.
DashboardsOne per service that answers “is it fine?” at a glance, and one per incident type that answers “what is wrong?”. Built before you need them, because nobody designs a dashboard well at three in the morning. The good test: could someone who has never seen this service tell from the top row whether to escalate?
Topic · 36

Incidents, on-call and the postmortem

Often asked as a story question — “tell me about an outage” — and scored on whether you separate mitigation from diagnosis, and blame from cause.

  1. Stop the bleeding first. Roll back, fail over, shed load, turn the flag off. Understanding can wait; the users cannot.
  2. Declare it and take a role. Someone commands, someone communicates, someone investigates. One person doing all three is how incidents get longer.
  3. Keep a timeline as you go. Written afterwards it is fiction. Written live it is the postmortem’s spine.
  4. Communicate on a fixed cadence, even with nothing new. Silence generates a second incident made of people asking questions.
  5. Find the cause afterwards, not during. The pressure to explain while mitigating is what produces the wrong fix.
  6. Write it up blamelessly, with contributing factors rather than a culprit, and action items that have owners and dates. A postmortem with no changed defaults changed nothing.
  • Tell me about the worst outage you were part of.whether you can separate what you did to stop it from what caused it, and whether you say what you would do differently.
  • You are paged at 3am: error rate is 40%. First five minutes?a triage order — what changed, is it all users or some, roll back if a deploy is in the window — not a debugging session.
  • How do you decide what to alert on?symptoms over causes, an action per page, and a stated view on alert fatigue.
  • What did you change after the last incident?whether the follow-up actually shipped. Most postmortem items do not, and admitting that is more credible than claiming otherwise.
Part ten

Security

Not a specialist subject any more. Every backend loop now contains at least one question about identity, one about input, and one about secrets — and a design round that never mentions authorisation reads as incomplete. Three topics cover almost all of it: who the caller is, what untrusted input can do, and what cryptography actually promises.

Topic · 37

Authentication and authorisation

Two words that are constantly swapped. Authentication is who are you; authorisation is what may you do. Most real breaches are failures of the second while the first worked perfectly.

THE AUTHORISATION CODE FLOW WITH PKCE, STEP BY STEP 1  browser → your app GET /login. Nothing secret has moved yet. 2  your app → browser Redirect to the provider, carrying a random state and a hashed code_challenge. 3  browser → provider The user authenticates and consents. Your service never sees the password. 4  provider → browser Redirect back to your callback with a short-lived, one-time code. 5  browser → your app GET /callback. Check state here, or you have built a login-CSRF hole. 6  your app → provider Back channel only: the code, the code_verifier, and the client secret. 7  provider → your app An ID token, an access token, and usually a refresh token. Verify all of them. 8  your app → browser Set a session cookie: HttpOnly, Secure, SameSite. Not the access token. PKCE exists because steps 4 and 5 travel through a browser. An attacker who steals the code cannot use it without the verifier, which never left your app. The implicit flow — a token straight back in the URL fragment — skipped all of this, which is why it is deprecated for every kind of client. Access tokens are for calling APIs. ID tokens are for knowing who signed in. Sending an ID token to an API is a common and wrong shortcut.
The two lines interviewers listen for are “check the state parameter” and “the token exchange happens server to server”. Everything else in the flow is redirect plumbing. If a candidate describes a login where the browser ends up holding a long-lived access token in local storage, the follow-up about cross-site scripting writes itself — any injected script can read it, and there is no way to revoke it in time.
// jwt.json — the two halves anyone holding the token can read.
// Base64url is an encoding, not a cipher. Nothing secret goes in here.
{                                    // header
  "alg": "RS256",                    // what it CLAIMS it was signed with
  "kid": "2026-08",                  // which key in the issuer's JWKS
  "typ": "JWT"
}
{                                    // payload — the registered claims
  "iss": "https://id.example.com",   // who minted it
  "sub": "usr_01JB8Z0X4K9M2QW7N6R3", // who it is about
  "aud": "https://api.example.com",  // who it is FOR — reject if that is not you
  "exp": 1788012000,                 // expires at, epoch seconds
  "nbf": 1788011700,                 // not valid before
  "iat": 1788011700,                 // issued at
  "jti": "01JB8Z1M4N7PQ2RS3TU4",     // unique id: one-time use, deny-lists
  "scope": "links:read links:write", // then whatever else you agreed on
  "tenant": "acme"
}
// signature = RS256( base64url(header) + "." + base64url(payload), private key )

The order of the checks is the security property. Every claim above is attacker-controlled text until the signature has been verified — so anything that reads a claim first, including choosing the algorithm from the header, is deciding on data the attacker wrote.

# verify.py — the whole of JWT verification, and it is all in the arguments.
claims = jwt.decode(
    token,
    key=jwks.key_for(unverified_header(token)["kid"]),  # 1. key named by kid
    algorithms=["RS256"],          # 2. YOUR allow-list, never token["alg"] —
                                   #    that is how alg:none and the RS256->HS256
                                   #    downgrade both get in
    issuer="https://id.example.com",
    audience="https://api.example.com",     # 3. the claim everyone forgets
    leeway=30,                     # seconds of clock skew you will tolerate
    options={"require": ["exp", "iss", "aud", "sub"]},   # absent != valid
)

# 4. Only now is anything in `claims` trustworthy. Authentication is finished;
#    authorisation has not started, and it is a different question:
if "links:write" not in claims["scope"].split():
    raise Forbidden()
link = links.get(link_id, tenant=claims["tenant"])   # and so is this one
// jwt.json — the two halves anyone holding the token can read.
// Base64url is an encoding, not a cipher. Nothing secret goes in here.
{                                    // header
  "alg": "RS256",                    // what it CLAIMS it was signed with
  "kid": "2026-08",                  // which key in the issuer's JWKS
  "typ": "JWT"
}
{                                    // payload — the registered claims
  "iss": "https://id.example.com",   // who minted it
  "sub": "usr_01JB8Z0X4K9M2QW7N6R3", // who it is about
  "aud": "https://api.example.com",  // who it is FOR — reject if that is not you
  "exp": 1788012000,                 // expires at, epoch seconds
  "nbf": 1788011700,                 // not valid before
  "iat": 1788011700,                 // issued at
  "jti": "01JB8Z1M4N7PQ2RS3TU4",     // unique id: one-time use, deny-lists
  "scope": "links:read links:write", // then whatever else you agreed on
  "tenant": "acme"
}
// signature = RS256( base64url(header) + "." + base64url(payload), private key )

The order of the checks is the security property. Every claim above is attacker-controlled text until the signature has been verified — so anything that reads a claim first, including choosing the algorithm from the header, is deciding on data the attacker wrote.

# verify.py — the whole of JWT verification, and it is all in the arguments.
claims = jwt.decode(
    token,
    key=jwks.key_for(unverified_header(token)["kid"]),  # 1. key named by kid
    algorithms=["RS256"],          # 2. YOUR allow-list, never token["alg"] —
                                   #    that is how alg:none and the RS256->HS256
                                   #    downgrade both get in
    issuer="https://id.example.com",
    audience="https://api.example.com",     # 3. the claim everyone forgets
    leeway=30,                     # seconds of clock skew you will tolerate
    options={"require": ["exp", "iss", "aud", "sub"]},   # absent != valid
)

# 4. Only now is anything in `claims` trustworthy. Authentication is finished;
#    authorisation has not started, and it is a different question:
if "links:write" not in claims["scope"].split():
    raise Forbidden()
link = links.get(link_id, tenant=claims["tenant"])   # and so is this one
Authentication vs authorisationAuthentication answers who are you and happens once per request at the edge; authorisation answers may you do this, to this object and happens at every decision point inside. They fail differently: a broken authenticator lets a stranger in, a broken authoriser lets a legitimate user read someone else’s row. Most real breaches are the second kind, because the first is a library and the second is a hundred call sites.
Sessions vs tokensA session is an opaque random id in a cookie; the state lives server-side, so signing out is a DELETE and the id is worthless the instant you do it. A signed token carries the state, so no lookup is needed on any hop — and nothing can be withdrawn before it expires. That asymmetry is the whole answer: sessions for a first-party web app, tokens for APIs and service-to-service, and if you want both, short expiry plus a refresh flow so the revocation window is minutes rather than days.
Cookie attributesFour of them decide how bad a cross-site scripting bug turns out to be. HttpOnly stops script reading the cookie at all; Secure stops it travelling over plain HTTP; SameSite=Lax, now the default, stops it riding along on cross-site POSTs, which removes most of CSRF; Domain and Path decide which of your hosts ever see it. Set all four and a stored XSS still cannot steal the session. Miss HttpOnly and it is one injected line.
What a JWT actually isThree base64url segments joined by dots: a header naming the algorithm and the key, a payload of claims, and a signature over the first two. Base64url is an encoding, not a cipher — anyone holding the token can read every claim, so no secret ever goes in one. The signature is the only part that needs a key, and it is the only part that means anything.
The registered claimsiss who minted it, sub who it is about, aud who it is for, exp and nbf the validity window in epoch seconds, iat when it was issued, jti a unique id for one-time use or a deny-list. aud is the one people skip and the one that matters: without it, a token minted for one of your APIs is happily accepted by another, and so is a token your identity provider issued to somebody else’s tenant.
Verifying one, in orderResolve the key by the header’s kid from the issuer’s JWKS endpoint, cached, refetching once on an unknown kid so key rotation does not page you. Check alg against your allow-list, never the token’s. Verify the signature. Only then read iss, aud, exp and nbf, with a few seconds of leeway for clock skew. A verifier that reads claims before checking the signature is deciding on attacker-supplied text.
The two classic JWT holesalg: none — a library that honours the algorithm the token names will accept an unsigned token as valid. And RS256 downgraded to HS256: the attacker re-signs using your public key as the HMAC secret, and a verifier that takes the algorithm from the header validates it happily. One line fixes both, and it is the same line: pin the algorithm server-side.
Refresh tokensLong-lived, so they are the valuable thing to steal. Rotate on every use and remember the one just retired — if a retired token is ever presented, the family has been cloned, so revoke the whole family and force a fresh login. Keep them where script cannot reach: an HttpOnly cookie in a browser, the keychain on a device, never local storage.
OAuth 2.0, and which grantIt is a delegation protocol: the user lets your app act for them without handing over a password. Authorisation code with PKCE for anything with a human, single-page apps and mobile included, where PKCE stands in for the client secret you cannot keep. Client credentials for machine-to-machine with no user at all. Device code for televisions and command-line tools. Implicit and password grants are deprecated, and being able to say why is the actual question.
OIDC on top of itOAuth alone hands you an access token and tells you nothing about the user — that token is not addressed to you and you should not parse it. OpenID Connect adds an ID token, a JWT about the user, plus a /userinfo endpoint and a discovery document. The rule of thumb: ID token to establish a session, access token to call an API. Sending an ID token to an API is the common and wrong shortcut.
SAML and enterprise SSOOlder, XML, and still what most enterprise customers mean by “SSO”. An assertion signed by the identity provider is POSTed to your assertion consumer service; you check the signature, the audience and the timestamps, then provision the account just-in-time on first login. The traps are XML signature wrapping, and the fact that SAML has no refresh concept — once the assertion is spent, the session is entirely yours to manage.
Password storageA slow, memory-hard hash with a per-user salt: Argon2id first, scrypt or bcrypt acceptable. Not SHA-256 — a GPU does billions a second, so a fast hash means a stolen table is cracked rather than merely leaked. Not encryption, which implies a key you can lose. The library generates the salt and stores it inside the hash string, so “where do you keep the salt” has a boring answer, and the work factor is tuned until one verification costs your server tens of milliseconds.
Multi-factor, and how it is beatenTOTP codes are phishable in real time by a proxy that relays them; push prompts get fatigued into approval at three in the morning; WebAuthn and passkeys are bound to the origin, which is the property that actually stops phishing. Whichever you pick, the weak point is nearly always account recovery — an SMS reset that skips the second factor is not a second factor, it is a slower first one.
Service-to-serviceNo user, so no password: mutual TLS where both ends present a certificate, or a workload identity exchanged for a short-lived token at a metadata endpoint. Both give you rotation for free and an identity the network itself can enforce. A long-lived shared key in an environment variable gives you neither, and it is in your logs the first time somebody prints a header while debugging.
Authorisation modelsRBAC maps users to roles to permissions: simple, and it degrades into two hundred roles nobody dares delete. ABAC decides from attributes — department, region, time of day — flexible and hard to reason about. ReBAC asks whether a relationship exists (“is this user an editor of the folder this document is in?”), which is what document-shaped and repository-shaped products actually need. Name the one the product shape calls for, not your favourite.
Where the check belongsRoute middleware can only answer “is this caller authenticated and broadly allowed here”. Object-level authorisation — is this row theirs — has to happen where the row is loaded, which is why the safe shape is a repository that takes the caller and filters, rather than a service that fetches and then remembers to compare. The bug is always an omission, and an omission is invisible in review: nothing is wrong on the screen, something is simply absent.
Least privilegePer environment, per service, per table, per operation. The reporting job does not need DELETE; the payments service does not need the users table. The half nobody does is the audit that proves it — an unused permission looks exactly like a needed one unless something measures, which is what access analysers and query logs are for.
Multi-tenant isolationEvery query filtered by tenant, enforced in one place — a session variable with row-level security, or a data layer no query bypasses — because three hundred call sites will eventually contain one that forgets. Test it with a deliberately hostile case: authenticate as tenant A, ask for a known id of tenant B, and assert a 404 rather than a 403, so the response does not confirm that the row exists.
Logout and revocationWith sessions it is a delete. With tokens it is the genuinely hard part: shorten the expiry, keep a deny-list of jti values until they lapse, or store a per-user “valid from” timestamp the verifier compares against iat. All three cost a lookup, which is the very property you bought tokens to avoid — say that trade out loud rather than claiming stateless revocation, which does not exist.
Impersonation and support accessStaff acting as a user needs a path of its own: a token carrying both identities (sub the user, act the operator), a recorded reason, a time limit, and an audit line you would be willing to show the user. Reusing the ordinary login makes every audit trail a lie. Asked routinely in product-company loops, and almost nobody has an answer ready.
Topic · 38

The application attack surface

Every hop between a user and your data has its own family of attacks. You are not expected to be a penetration tester — you are expected to know what class of bug lives where, and the standard defence for each.

WHERE EACH FAMILY OF BUG LIVES browser edge and CDN API gateway your service data store cross-site scripting request forgery clickjacking tokens in local storage volumetric floods cache poisoning open redirects request smuggling broken authentication missing rate limits insecure direct refs mass assignment server-side forgery unsafe deserialising command injection path traversal SQL injection over-broad grants unencrypted at rest backups nobody guards Cutting across every column: vulnerable dependencies, secrets committed to a repository, tokens written to logs, and a build pipeline anyone can change. Two defences answer most of the grid: never build a command from a string, and check authorisation on the object, not on the route. Validate input at the boundary, encode output at the sink. The two are different jobs and doing only the first is the usual mistake.
The one that keeps getting through is the third item in the gateway column. An insecure direct object reference is not a clever exploit — it is changing /invoices/1042 to /invoices/1043 and being shown someone else’s invoice, because the code checked that you were logged in and never checked that the row was yours. It survives code review because the missing line is invisible: nothing is wrong on screen, something is simply absent.
# The two lines that answer half of an application-security question.

# 1. Never build a query by concatenation. The parameter is data, forever,
#    no matter what it contains.
cur.execute("SELECT id, email FROM users WHERE tenant_id = %s AND email = %s",
            (tenant_id, email))          # not f"... email = '{email}'"

# 2. Authorise the object, not the route. A logged-in user is not the same
#    thing as this user, and the row is where the truth is.
invoice = repo.get(invoice_id)
if invoice is None or invoice.tenant_id != caller.tenant_id:
    raise NotFound()                     # not Forbidden: do not confirm it exists
InjectionAny place a value is concatenated into a language the receiver will parse — SQL, shell, LDAP, a template, a NoSQL query document. The fix is never escaping: it is separating code from data with a parameterised query, an argument array instead of a shell string, or a template engine that does not evaluate. Escaping is a blocklist, and blocklists lose.
Cross-site scriptingAttacker script running on your origin, so it can do anything the user can. Three shapes: stored (saved and served to everyone), reflected (echoed back from a parameter), DOM-based (your own JavaScript writes untrusted data into innerHTML). The fix is contextual output encoding at the sink — HTML, attribute, URL and JavaScript contexts each need different escaping — plus a Content Security Policy as the second line, and HttpOnly so the session survives one anyway.
Cross-site request forgeryAnother site causes the browser to make an authenticated request to yours, using cookies it sends automatically. SameSite=Lax as a cookie default removed most of it; a per-session synchroniser token in a header removes the rest, because a cross-origin page cannot read your response to obtain one. Note the interaction: an API authenticated by an Authorization header rather than a cookie is not vulnerable, and one that accepts either is.
Server-side request forgeryYou fetch a URL the user supplied, and they point it at 169.254.169.254 or an internal admin service. Devastating in a cloud environment, because the metadata endpoint hands out credentials. Defence is an allow-list of destinations, resolving the name and validating the resulting IP (not the string), refusing redirects to private ranges, and denying the fetcher egress by default.
Broken access controlThe largest category in real breaches and the least interesting to look at: nothing appears wrong, a check is simply absent. Insecure direct object reference is the everyday form — change /invoices/1042 to 1043 and read someone else’s. Guessable identifiers are not the bug and unguessable ones are not the fix; the fix is checking that the object belongs to the caller, at the point the object is loaded.
Mass assignmentBinding a request body straight onto a model, so the user adds "role": "admin" or "account_balance": 999999 to the JSON and your ORM helpfully saves it. Fix with an explicit input type listing exactly the fields a client may set — never a blocklist of the ones it may not, because the next field somebody adds is not on it.
Insecure deserialisationFormats that reconstruct arbitrary object graphs — Java serialisation, Python pickle, unsafe YAML loaders — can be crafted to execute code during deserialisation, before any of your validation runs. Never deserialise untrusted input with them. Use a data format with no code semantics, and if you must accept typed payloads, use a strict allow-list of permitted classes.
Path traversal and file handling../../etc/passwd in a filename, a zip entry that escapes its directory, or an upload whose extension decides how the web server executes it. Resolve the path and verify it is still inside the intended directory, generate your own filenames rather than trusting the client’s, and serve user uploads from a separate origin so a crafted file cannot run as your site.
XML external entitiesAn XML parser that resolves external entities will read local files or make network requests on the attacker’s behalf. It is a configuration bug rather than a coding one: disable DTD processing and external entities, which most modern parsers now do by default — which is exactly why an old service or an old library version is where it still lives.
Open redirects and clickjackingA ?next= parameter that will forward anywhere makes your domain a credible starting point for a phishing link; validate it against an allow-list of paths on your own site. Clickjacking frames your page invisibly over something the user is being tricked into clicking; X-Frame-Options: DENY or a CSP frame-ancestors directive ends it.
Business logic flawsNo injection, no missing authorisation — the system does exactly what it was built to do and that turns out to be wrong. A negative quantity that credits the account, a discount code applied twice by two concurrent requests, a refund of more than was paid, a workflow whose steps can be done out of order. Scanners never find these; they are found by asking “what if I do this twice at once?” about every state change.
Race conditions in the flowTwo requests arrive together and both pass a check that should have allowed one: two withdrawals against one balance, two redemptions of a single-use voucher, two signups for one seat. The check-then-act is the bug and the fix is a database constraint, a conditional update, or a lock on the thing being protected — not a shorter window.
Enumeration and timingDifferent responses for “no such user” and “wrong password” hand an attacker a list of valid accounts; so does a signup form that says the email is taken, and so does a 403 where a 404 was safer. Timing leaks the same thing more quietly, which is why credential checks use constant-time comparison and why a failed lookup should still cost a hash.
The supply chainThe dependency you did not audit, the transitive one you have never heard of, a typosquatted package name, a build step that runs an install script, and a CI token with write access to the registry. Practical answers: lock files with integrity hashes, a pinned internal proxy, automated vulnerability scanning with a policy for acting on it, and treating the pipeline itself as production infrastructure.
Secrets and logsA key in the repository is compromised the moment it is pushed, and deleting the commit does not remove it from the history — the only response is rotation. The subtler version is logging: a token in a URL ends up in access logs, a request body dumped on error ends up in your log store, and both are now in a system with much broader access than the original secret.
Defence in depthNo single control is trusted to hold. Validate input at the boundary and encode output at the sink; authenticate at the edge and authorise at the object; restrict egress and allow-list destinations. The question interviewers use to test this: “that control fails — what is the next thing that stops the attack?” If the answer is nothing, the design is one bug deep.

The vocabulary

SQL injectionNoSQL injectioncommand injectionLDAP injectioncross-site scriptingstored, reflected, DOMcontent security policycross-site request forgerySameSite cookiesserver-side request forgeryinsecure deserialisationXML external entitiespath traversalbroken access controlinsecure direct object referenceprivilege escalationmass assignmentopen redirectclickjackingrequest smugglingsubdomain takeoverrace condition in checkouttiming attackenumerationcredential stuffingsession fixationreplay attacksupply chain attacktyposquattingdependency confusionsecrets in git historylog injectionprompt injectionrate limit bypassbusiness logic flaw
Topic · 39

Cryptography, at the level you are expected to use it

Nobody expects you to implement a cipher. They expect you to know which primitive solves which problem, and to never reach for one you have written yourself.

HashingOne way, fixed size, deterministic. SHA-256 for integrity and for identifiers; the properties that matter are pre-image resistance and collision resistance, which is why MD5 and SHA-1 are dead for anything adversarial while still fine as a cache key. Use a constant-time comparison when checking a digest against a user-supplied one, or the comparison itself leaks the answer a byte at a time.
Password hashingA different job with a different tool: deliberately slow and memory-hard, so that a stolen table costs years rather than an afternoon on rented GPUs. Argon2id first (tune memory, iterations and parallelism), scrypt or bcrypt acceptable, and bcrypt silently truncates past 72 bytes. Per-user salt, stored inside the hash string by the library. A pepper — a secret added from configuration — is a reasonable extra, not a substitute.
MAC and HMACIntegrity plus authenticity from a shared secret: the recipient can tell the message is unmodified and came from someone with the key. The right tool for webhook signatures, signed URLs and cookie integrity. Two details that get asked: include a timestamp in the signed payload so a captured request cannot be replayed, and compare with a constant-time function.
Symmetric encryptionAES-GCM or ChaCha20-Poly1305, both authenticated, so tampering is detected rather than decrypted into garbage. Never CBC without a separate MAC — that is where padding-oracle attacks live. The rule that breaks systems: never reuse a nonce with the same key, which with a 96-bit random nonce means rotating the key well before you have written enough messages for a birthday collision.
Asymmetric encryptionA public key anyone may hold and a private key you do not share. Orders of magnitude slower than symmetric, so it is used to exchange or wrap a symmetric key rather than to encrypt data — which is what TLS does in its handshake, and what envelope encryption does at rest. Elliptic curve gives equivalent strength to RSA at much smaller key sizes.
SignaturesProve origin and integrity without a shared secret: sign with the private key, verify with the public one. This is what a JWT relies on, what code signing relies on, and what lets a client verify a token issued by a service it has no secret with. Anyone can verify, which is exactly the property a shared HMAC key cannot give you.
TLS and PKIA certificate binds a name to a public key, signed by an intermediate, chained to a root your operating system trusts. What it proves is that you are talking to whoever controls that name — not that they are honest, and not that the data is safe after it arrives. Common failures: a chain missing its intermediate (works in a browser, fails in your client), an expired certificate, a wrong clock, and a container image with no root store at all.
Key managementThe actual hard part. Where keys live (a managed KMS or an HSM), who can use them, how they rotate without breaking data encrypted under the old one, and how a compromise is contained. Envelope encryption is the standard shape: a data key encrypts the data, a master key encrypts the data key, so rotating the master is cheap and the master never leaves the module.
RandomnessCryptographically secure generators only — secrets, crypto/rand, SecureRandom. A session id, a password reset token or an API key from a fast pseudo-random generator is predictable given a few samples, and Math.random has produced real breaches. Also size it: 128 bits of entropy is the floor for anything that acts as a credential.
At rest versus in transitTwo different threats. Disk encryption protects against someone walking off with the drive and does nothing at all against a compromised application, which reads through it as normal. Field-level encryption protects specific data from your own database administrators and from a dump; it costs you the ability to index or search that field. Say which threat you are addressing.
Tokenisation and minimisationThe cheapest way to protect regulated data is not to hold it: swap a card number for a token from a provider, keep only the last four digits, and let the compliance scope shrink accordingly. The same instinct applies generally — every field you do not store is a field that cannot leak, and every retention window you shorten reduces the size of an eventual breach.
Privacy obligationsData minimisation, purpose limitation, retention limits, the right to deletion and to export, and where the data may physically live. These are architectural constraints, not paperwork: “delete this user” has to reach backups, logs, analytics, caches and the search index, which is a design decision you have to have made in advance.
  • How would you store passwords?a slow hash by name, a per-user salt, and the reason a fast hash fails. Mentioning a pepper or a work factor is a bonus, not the point.
  • A user reports seeing another user’s data. What happened?a missing object-level authorisation check, a cache keyed without the tenant, or a connection pool leaking context. All three are worth naming.
  • How do you handle secrets in this design?a secret manager, short-lived credentials, rotation, and nothing in the repository or the image.
  • What does TLS actually protect against here?the distinction between transport security and everything else. It does not authenticate the user or protect the data once it arrives.
Part eleven

The other loops

Everything up to here is the shared core. A specialist loop keeps most of it and swaps one or two rounds for something domain-specific. This part says what each track adds, so that a backend engineer interviewing for a platform role knows what the fourth round is going to be.

Topic · 40

Which rounds each track actually runs

The mix changes more than the subject. A frontend loop keeps the coding round but is likely to make it a component; an infrastructure loop replaces low-level design with a practical exercise on a broken machine.

WHICH ROUNDS EACH TRACK RUNS CODING SYSTEM LOW-LEVEL DOMAIN PRACTICAL BEHAVIOURAL backend core core core sometimes sometimes sometimes frontend core core core core sometimes sometimes mobile core core core core sometimes sometimes data engineering core core core core sometimes machine learning core core core core sometimes infrastructure core core core core sometimes security core core core sometimes sometimes An empty cell is not a promise. Ask the recruiter for the round list — every company will tell you, and most will tell you the format too.
The domain round is the one that varies most, and it is the one candidates prepare least. For a frontend engineer it is rendering, state and accessibility; for a data engineer it is pipelines and correctness under replay; for an infrastructure engineer it is a broken machine and a terminal. Preparing the shared core and skipping this column is how strong generalists fail specialist loops.
Topic · 41

What each track adds on top

The subjects below are additional to everything in parts two to ten — not a replacement for them. A frontend loop still asks about hash maps.

FrontendThe rendering path — parse, style, layout, paint, composite — and which changes force which stage, because that is why a transform is cheap and a width is not. The critical path, bundle size, code splitting, and what hydration costs on a mid-range phone. State management and reactivity. Accessibility as a real subject: semantic elements, focus order, keyboard-only operation, ARIA only where semantics run out. Core Web Vitals. CSS layout, specificity and the cascade. Forms, validation and optimistic updates. Then internationalisation and the browser support matrix.
MobileThe activity or view-controller lifecycle, and exactly which state is lost on rotation or backgrounding. Memory pressure and being terminated without warning. Offline-first storage, a sync engine, and conflict resolution when two devices edited the same thing. Background execution limits, which are strict and differ by platform. Push notifications end to end. Battery and cellular cost as design constraints. And the release model: app size, store review, staged rollout, and the fact that you cannot force an update — so the API must support the version from eighteen months ago.
Data engineeringBatch against streaming, and event time against processing time — with watermarks and a policy for late data, because the answer “we drop it” is fine if you say it. Pipelines that are idempotent and replayable, so a backfill is routine rather than terrifying. Partitioning schemes, columnar file formats, compaction and the small-files problem. Schema evolution and who breaks when a producer adds a field. Orchestration, dependencies and retries. Data quality tests, lineage, and the cost of a full reprocess in both money and hours.
Machine learning engineeringTraining and serving skew, and the feature store that exists to prevent it. Offline metrics against online ones, and why they disagree. Drift, and what triggers a retrain. Deployment: shadow traffic, canaries, and rolling back a model like any other artifact. Latency and cost per inference, batching, caching, quantisation, and hardware choice. For language-model work: context management, retrieval, tool use, evaluation harnesses that are not vibes, and prompt injection as a genuine security boundary rather than a content-filter problem.
Infrastructure and SREA terminal and a broken host, under time pressure — Linux internals, networking, DNS and TLS debugging with the standard tools and no internet search. Capacity planning and load testing with numbers. Terraform, drift and the blast radius of a plan. Kubernetes internals rather than its YAML: the scheduler, the controllers, the network model, what actually happens during a rolling update. On-call practice, error budgets, and the failure injection you have actually run rather than the one you have read about.
Security engineeringThreat modelling a design out loud — assets, entry points, trust boundaries, and what an attacker does after the first foothold. Reading code for vulnerabilities rather than running a scanner over it. Thinking in attack chains, because a low-severity information leak plus a low-severity redirect is frequently a critical. Detection and response: which log sources exist, what a good detection looks like, and how you would know. Compliance frameworks treated as constraints on architecture rather than as documents.
Test and quality engineeringTest strategy across a system rather than a file: what is covered where, and what is deliberately not automated. Automation frameworks and their patterns, flakiness at a scale where a 0.1% failure rate means a red build every run, and test data management for environments that must not contain real customer data. A considered position on what should stay manual — exploratory testing, usability, and anything whose oracle is a human judgement.
Embedded and systemsMemory constraints measured in kilobytes, so no allocation in the hot path and often none at all. Interrupts, and what may safely happen inside one. Real-time deadlines, hard and soft. Fixed-point arithmetic where there is no floating-point unit. Hardware interfaces, cross-compilation and the toolchain. And firmware update as the defining problem: it must be atomic, verified, power-fail safe, and impossible to brick, because there is no engineer standing next to the device.
Platform and developer toolingBuild systems, dependency graphs, and caching that is correct as well as fast. Monorepo tooling and change detection. Developer experience treated as a product with users who can choose not to adopt: adoption metrics, migration paths, and codemods rather than instructions. The distinctive skill is migrating hundreds of teams without stopping them — which means the new path has to be easier than the old one, because nobody reads the documentation.

The machine-learning column has a page in this series

Prompt to Production covers what an AI engineer is hired to know — tokens, embeddings, retrieval, agents, evaluation, serving and cost — with fifteen interactive figures. It is the deep dive behind one line of the grid above.

Part twelve

The human rounds

Scored on the same rubric as the technical ones, with the same seriousness, and prepared for by almost nobody. At senior level they carry as much weight as a coding round, because the question they answer — what is this person like to work with when something goes wrong — is the question the team actually cares about.

Topic · 42

Behavioural questions and the story bank

The format is fixed and the prompts are recycled. Five well-chosen stories, written down once, cover almost every question you will be asked in any company.

HOW LONG EACH PART OF THE ANSWER SHOULD BE situation task action — what YOU did, decision by decision result, measured Situation — one sentence. Where, when, what the system was. Nobody needs the org chart. Task — one sentence. What was yours to solve, and what made it hard. Action — most of the answer. Say “I”, not “we”. Include the option you rejected and why. Result — a number if one exists, and one sentence on what you would do differently. FIVE STORIES COVER THE PROMPTS — A FILLED MARK IS A STORY THAT ANSWERS THAT ONE conflict failure ambiguity influence impact learning the migration that slipped the outage I caused the design I argued against the project nobody owned the teammate I disagreed with Every column has at least two marks, which is the point: no single prompt can leave you without a story, and no story has to be reused twice in one loop.
Write the five stories out in full, once, and never again. The writing is what surfaces the parts you have forgotten — the number, the option you rejected, the person you had to convince — and those are exactly the details that turn a story about a task into a story about a decision. Rehearsing delivery is much less useful than rehearsing recall.

The prompts, in the words they are usually asked

tell me about yourselfa project you are proud ofa time you disagreed with someonea time you were wrongyour biggest failurea deadline you misseda time you had to say noan ambiguous problema decision with incomplete informationa time you influenced without authorityconvincing a skeptical stakeholdermentoring someonedifficult feedback you gavedifficult feedback you receiveda conflict with a manageran underperforming teammategoing beyond your rolesimplifying somethinga technical trade-off you madethe most complex system you builta production incidenta bug that took daysprioritising under pressurehandling a change of directionworking with a difficult teamsomething you learned recentlywhy are you leavingwhy this companywhere do you want to be in three yearswhat would your manager say about you

Three answers that reliably cost marks

“We” throughout. The interviewer cannot score a team. Say what you personally decided, even when the work was shared. A failure that is secretly a success. “I care too much about quality” reads as evasion; a real failure with a real lesson reads as someone who can be told things. A conflict story where you were simply right. The scored part is how you handled the disagreement, so a story that ends with the other person being wrong has skipped the answer.

Topic · 43

Seniority: what senior and staff signals actually sound like

Level is not years, and it is not the size of the system you worked on. It is the size of the ambiguity you were handed and what you did with it.

DimensionMidSeniorStaff and beyond
Scope of workA well-specified task or feature.A vague problem, turned into a plan and delivered.A problem area nobody had framed, including whether to solve it at all.
AmbiguityAsks for the requirements.Writes the requirements and gets them agreed.Notices the requirement everyone assumed and challenges it.
Technical decisionsFollows the existing pattern.Chooses between patterns and can defend the choice.Decides which decisions need to be made now and which to defer.
InfluenceOwn work.The team’s work; reviews, mentoring, standards.Across teams, by writing and by making the good path the easy one.
RiskRaises it.Mitigates it and communicates the residue.Trades it deliberately against time and cost, in public.
FailureReports it.Owns it, fixes it, changes the default that allowed it.Changes the system so the class of failure stops happening.
CommunicationClear updates.Writes documents others use to decide.Aligns people who disagree, without authority over them.
  • What is the most impactful thing you have built?whether impact is measured in your answer, and whether you can say who benefited rather than what shipped.
  • Tell me about a decision you made that turned out badly.ownership without self-flagellation, and a concrete change afterwards. This is the single highest-signal behavioural question there is.
  • How do you decide what to work on?a prioritisation model of any kind, and evidence you have said no to something.
  • How would you onboard onto a codebase you have never seen?method: run it, read the tests, trace one request, fix something small, ask specific questions.
Topic · 44

Your questions, the offer, and the decision you are also making

The last five minutes of every round are yours. They are scored — and they are also the only chance you get to find out whether you want the job.

Ask about the work“What has the team shipped in the last quarter, and what is next?” A specific answer tells you the roadmap is real and the person is close to it. A vague one — “lots of exciting things” — is the single most reliable warning sign in the whole process, and it is worth following up once rather than letting it pass.
Ask about the codeHow long does CI take, how often do you deploy, what does on-call look like, how big is an average pull request, what proportion of a sprint goes on maintenance? These are unglamorous and they predict your daily experience far better than the stack does. A team that deploys twice a day and one that deploys monthly are different jobs with the same job title.
Ask about failure“Tell me about a recent incident and what changed afterwards.” The answer tells you whether postmortems are real or ceremonial, whether the changes shipped, and whether people are comfortable describing a mistake to a stranger. Reluctance here is itself the answer.
Ask about the personWhat they enjoy, what frustrates them, why they have stayed. People answer this honestly far more often than you would expect, because it is the first question all day that has been about them. It is also how you find out what the team is actually like, as opposed to what the careers page says.
Ask about the levelWhat separates this level from the next one here, what the last promotion at this level looked like, and how performance is assessed. If the answer is fuzzy at the hiring-manager stage, it will be fuzzy at your review. Ask before the offer, not after.
Do not ask what you could have readAnything on the careers page, in the last funding announcement or in the job description wastes a slot you do not get back — and it reads as not having prepared. Three good questions beat six generic ones, and it is fine to say you have already read the obvious things.
CompensationRanges early, specifics at the offer, and get the whole shape: base, bonus target and how often it actually pays, equity with a share count and a strike price or current valuation, the vesting schedule and cliff, and whether there is a refresh. Equity quoted only as an annual dollar figure is not a number you can evaluate, and asking for the components is completely normal.
The offer conversationNegotiating politely is expected and is not held against you; the range you were told is usually not the ceiling. Competing offers are the strongest lever, genuine enthusiasm is the second, and a specific ask (“could we get the base to X?”) beats a vague one. Get the final terms in writing, and read the parts about notice, intellectual property and any repayment clause.
RejectionAsk for feedback, expect little — most companies give none for legal reasons rather than unkind ones — and ask about the cooling-off period, which is usually six to twelve months. Re-application after that is common and frequently succeeds, particularly if you can point at the specific thing you went away and fixed.
WithdrawingDoing it early and politely costs nothing and is remembered. Say you are withdrawing, thank them for the time, and do not invent a reason. Interviewing communities are small: the person you decline this year may be the hiring manager you meet in three, and a graceful exit is worth more than a burnt bridge is worth avoiding.
Part thirteen

Preparing, without quitting your job

The subject is large and the time is not. What follows is the allocation that actually works for someone with a full-time job: fundamentals early, spaced practice throughout, design and stories in the middle, and mocks late enough to be useful but early enough to act on.

Topic · 45

Eight weeks, allocated

Not a syllabus — a shape. Compress it or stretch it, but keep the order: the practice stream has to start before you feel ready, and the mocks have to start before you feel ready twice.

EIGHT WEEKS, FIVE STREAMS, ABOUT AN HOUR A DAY w1 w2 w3 w4 w5 w6 w7 w8 gaps the words you did not know on this page problems a few a week, revisited on a spacing schedule, one pattern at a time design one problem a week, written out end to end, then compared with a worked answer stories five of them, written in full, once mocks with a person, timed, out loud, recorded The two streams that overlap for six weeks are deliberate: solving problems and designing systems use different muscles and reinforce each other. If you have two weeks rather than eight, keep all five streams and shrink each — dropping the mocks entirely is the most expensive saving available.
The first stream is the one this page is for. Read the chip walls, mark every term you could not explain to someone else, and turn that list into the plan — it will be far shorter than a generic curriculum and every item on it is a real gap. Coverage first, then depth, then speed; doing them in any other order means practising what you already know.
Topic · 46

Practising the thing rather than around it

Reading solutions feels like progress and is not. The gap between knowing an algorithm and performing it while narrating, under observation, in forty-five minutes, is the entire skill being tested.

Out loud, alwaysSolve at least half your practice problems while narrating. It is startlingly harder — the part of your brain that solves and the part that explains compete for the same working memory — and that competition is precisely the skill the round measures. Practising silently trains the wrong thing well.
TimedSet forty-five minutes and stop when it rings, finished or not. Practising without a clock teaches you to finish late, and finishing late is a fail. Stopping mid-problem also teaches you the more useful skill: how to summarise where you got to and what you would do next, which is what you will actually be doing at minute forty-four.
Spaced repetitionRevisit a problem after a day, then a week, then a month. Recall is the thing being trained and it decays on a predictable curve; the uncomfortable feeling of half-remembering is the moment the learning happens. Twenty problems revisited four times beats eighty solved once, by a wide margin.
Retrieval, not reviewClose the solution and reconstruct it from nothing on a blank page. Re-reading a solution you already understood is the least efficient study there is: it feels productive because it is fluent, and fluency is exactly the illusion being measured against you when the editor is empty.
By pattern, not by listDo six sliding-window problems in a row and the shape sticks; do six random ones and nothing does. Work a family at a time until you can recognise the cue in the statement, then move on. The goal is not two hundred solved problems, it is fifteen patterns you can identify in thirty seconds.
Write real codeIn an editor, compiled and run against your own edge cases. Pseudocode hides exactly the bugs the round will find — the off-by-one, the uninitialised map, the wrong return on the not-found path. At least a few sessions should be in a plain text box with no autocomplete, because several interview platforms are precisely that.
Mock with a personA peer, a colleague, or a paid interviewer. The awkwardness is the training: being watched, being interrupted, being asked a question mid-thought, and having to recover. A solo mock trains none of that, and it is the part that goes wrong first when it counts.
Record oneUnpleasant and immediately useful. Most people discover they go silent for minutes at a time, say “so” before every sentence, or spend eight minutes on the brute force they intended to mention in one. You only need to watch one to fix half of it.
Design out loud tooExplain a system you know well to someone who does not, on a whiteboard, in forty-five minutes. It is the same skill the design round measures, and it is the only way to find out that the thing you have worked on for two years is something you cannot actually explain.
Keep a mistake logOne line per error: what you missed and what class it was. Patterns appear within two weeks — always the empty case, always forgetting to state complexity, always starting to code before the approach is agreed — and a list of your own three recurring failures is worth more than any generic checklist.
Schedule the loopsPut a company you care less about first. The first loop of a search is always the worst one, for everyone, and spending it on the job you want most is an expensive way to warm up. Two or three loops in flight at once also gives you the timing leverage that makes an offer negotiable.
Rest before the daySleep beats one more problem by a margin that is not close: working memory, recall and the ability to talk while thinking are the first three things that degrade when you are tired, and they are the three things being scored. Nothing learned the night before survives contact with the round anyway.

What to do on the day

Eat something. Have water, paper and a pen. Close everything except the tools you were told to use. Read the problem twice before speaking. When your mind goes blank — and it will, in one round out of five — say so plainly and start from what you do know; interviewers have all been there and it is scored far more kindly than silence. Afterwards, write down the questions you were asked while they are fresh. That list is the most accurate study guide you will ever have, and it is worth keeping even after you take the offer.