Map · every topic a technical interview can reach
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.
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.
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.
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.
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.
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.
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.
“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.
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.
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.
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.
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
Trees, heaps and search structures
Graphs, sets and probabilistic structures
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
Recursion, greedy and dynamic programming
Graph algorithms
Strings, maths and the rest
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.
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.
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
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.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.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.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.
| What they ask | What a weak answer sounds like | What 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 complexity | Silence, or “O(1)” while holding a map | The output, the auxiliary structures and the recursion stack, counted separately. |
| Worst vs average | Quoting 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).” |
| Recurrence | Guessing | T(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
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.
(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.-1 and a function that throws are different contracts, and mixing them is what makes a caller wrong.[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.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.
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.
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.
if — when the condition is genuinely random.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.Also fair game
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.
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.EMFILE or “too many open files” under load and never in testing. lsof and /proc/<pid>/fd tell you which ones.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”.SIGTERM 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.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.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.
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.
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 } }
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.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.volatile in Java (which does establish ordering) is a different keyword from volatile in C (which does not).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.“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.
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.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.bind, 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.X-Forwarded-For is for and why trusting it blindly is a security bug.curl.dig 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.
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.
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.
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.
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.UNIQUE ... WHERE deleted_at IS NULL) is the trick that makes “unique among the live rows” expressible.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.COUNT(*), COUNT(col) and COUNT(DISTINCT col) are three different questions: the second skips NULLs, the third is expensive.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.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 = 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.UNION 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.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.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.CONCURRENTLY, takes a lock on a big table and stops the site.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.
“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.
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
ORDER BY without a sort, which is often the bigger win: it removes the memory-hungry step, not just the scan.(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.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.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”.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.LIKE '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.EXPLAIN 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.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.
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
fsync reached a disk, so replication mode and disk cache settings quietly redefine it.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”.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.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.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.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.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.
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.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.“Which database would you use?” is a trap only if you answer with a product. Answer with an access pattern and the product follows.
| Family | Use it when | What it costs you |
|---|---|---|
| Relational | The data has relationships, you need transactions, and queries will change. | Scaling writes past one machine; schema changes on huge tables. |
| Key–value | You always know the key, and the value is opaque. | No queries you did not plan for; no joins at all. |
| Wide column | Enormous write volume, queries fixed at design time. | The partition key is forever, and modelling is per query. |
| Document | Aggregates read and written whole; a schema that varies by tenant. | Duplication, and consistency across documents is yours to keep. |
| Search index | Text relevance, facets, fuzzy matching, aggregations over free text. | Eventual consistency with the source of truth, and reindexing. |
| Cache | Reads dominate, and staleness is acceptable and bounded. | Invalidation, stampedes, and a cold start that can take the system down. |
| Time series | Append-only measurements, queried by range and rolled up. | Cardinality explodes if a label is user-controlled. |
| Graph | The query is about paths and neighbourhoods, not rows. | An operational niche; most graph questions are answered fine by SQL. |
| Object store | Large immutable blobs, cheap and durable, served by URL. | Latency, listing costs, and no partial updates. |
| Columnar warehouse | Analytics over billions of rows, few columns at a time. | Not for a request path; loads in batches, answers in seconds. |
| Log / stream | Ordered, replayable events consumed by many independent readers. | Retention, partition ordering only, and consumer lag as a new thing to watch. |
| Embedded | Single 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.
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.
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.
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.
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.
Named patterns worth being able to define
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.
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.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.
| Problem | The decision it exists to force |
|---|---|
| URL shortener | Key generation without coordination, and a read path that is almost pure cache. |
| Pastebin / file share | Where the bytes live versus where the metadata lives, and signed URLs. |
| Rate limiter | Which algorithm, and where the counter lives when there are fifty API servers. |
| News feed / timeline | Fan-out on write against fan-out on read, and the celebrity who breaks whichever you chose. |
| Chat / messaging | Connection state, delivery receipts, ordering per conversation, and offline devices. |
| Notification service | Fan-out, deduplication, per-channel retries, and user preferences as a filter. |
| Typeahead / autocomplete | Precomputation, a trie or an index, and a latency budget of tens of milliseconds. |
| Web crawler | Politeness, frontier scheduling, deduplication at enormous scale, and traps. |
| Search engine | Inverted index, ranking, sharding by document, and index freshness. |
| Video streaming | Transcoding pipeline, adaptive bitrate, CDN economics, and cold storage of originals. |
| Cloud drive / sync | Chunking, deduplication, conflict resolution, and delta sync over a bad network. |
| Ride hailing | Geospatial indexing, matching under contention, and location updates as a write flood. |
| Food delivery | Three-sided state machine, and the fact that the real world can reject your transaction. |
| Ticket booking | Overselling. Holds, expiry, and a transaction boundary you can defend. |
| Payments / ledger | Double-entry, idempotency, exactly-once as an application property, and reconciliation. |
| Digital wallet | Balance as a derived value, not a mutable field. Auditability over convenience. |
| Ad click aggregation | Streaming aggregation, late events, watermarks, and approximate versus exact counts. |
| Metrics / monitoring | Time-series storage, cardinality, downsampling, and query cost at read time. |
| Log aggregation | Ingest volume, retention tiers, and search over data you cannot afford to index fully. |
| Distributed cache | Placement, consistent hashing, eviction, and what happens when a node leaves. |
| Key–value store | Replication, quorums, conflict resolution, and the storage engine underneath. |
| Object store | Durability arithmetic, erasure coding, and metadata as the real scaling problem. |
| Job scheduler | Exactly-once triggering, clock skew, long-running work, and failure mid-job. |
| Leaderboard | Sorted sets, approximate ranks at scale, and time-windowed resets. |
| Collaborative editor | Operational transformation or CRDTs, and presence as a separate problem. |
| Stock exchange | A single-threaded matching engine, determinism, and why sharding is the wrong instinct here. |
| Proximity search | Geohashing or quadtrees, and the query that crosses a cell boundary. |
| Recommendation feed | Offline candidate generation, online ranking, and a latency budget split between them. |
| A/B testing platform | Deterministic bucketing, exposure logging, and not letting analysis change the assignment. |
| Feature flags | Propagation delay, fail-open defaults, and a service that must never be down. |
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.
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.
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.
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()); } }
Reciting the five names scores nothing. Naming the smell each one describes, and the cost of ignoring it, is what the question is for.
The neighbouring vocabulary
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.
switch into a set of small testable classes — and because it makes the seam the interviewer will ask you to test against.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.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.@Transactional, and why that surprises everyone once.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.
| Exercise | The trap |
|---|---|
| Parking lot | Subclassing by spot size. It is an enum and a capacity rule, not a hierarchy. |
| Elevator system | Modelling the car without the scheduler. The interesting object is the dispatcher. |
| Vending machine | Money and state in the same class. It is a state machine with a coin ledger beside it. |
| ATM | Forgetting that dispensing can fail after the balance has changed. |
| Library / rental | Copies versus titles. Two entities that beginners merge into one. |
| Chess or tic-tac-toe | Rules scattered across pieces. A move validator with a board is easier to test and to extend. |
| Deck of cards | Shuffling correctly. Fisher–Yates, and a seeded random for testability. |
| LRU cache | Hash map plus doubly linked list, and the eviction that must be O(1) with the lock held once. |
| Rate limiter | Where the state lives, and what happens on a clock jump. |
| In-memory key–value store | Expiry: lazy on read, or a sweeper, and what each costs. |
| Logging framework | Levels, appenders and formatters as three separate abstractions, plus asynchronous flushing. |
| Notification service | Channels as strategies, preferences as a filter, and retries per channel. |
| Splitwise / expense sharing | Balances as a derived graph, not stored pairwise. Simplifying debts is the follow-up. |
| Food ordering / cart | Pricing rules as composable strategies; totals recomputed, never stored mutable. |
| Ride matching | Driver state transitions, and matching as a service the driver object does not own. |
| File system | Composite pattern, path resolution, and permissions inherited down the tree. |
| Text editor / undo | Command objects and a stack. Ask whether redo is in scope before designing for it. |
| Job scheduler | A priority queue plus workers, and what a task retry does to ordering. |
| Snake / game loop | Tick-based state updates, and keeping input, state and rendering apart. |
| Hotel or flight booking | Inventory holds with expiry — the same overselling problem as the system-design version, in one process. |
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.
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.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.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.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.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.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.curl, and a schema registry to run. Excellent between your own services, awkward as a public API.202 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.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" }
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.
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.
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.
| Language | What gets asked |
|---|---|
| Java | JVM 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. |
| Python | Names 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. |
| Go | What 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 / TypeScript | The 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. |
| Rust | Ownership, 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. |
| Kotlin | Null safety and platform types; coroutines, scopes and structured concurrency; data classes; extension functions; sealed classes and exhaustive when; interoperability with Java. |
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.
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.
“How would you test this?” follows almost every coding answer. The good response names layers and categories, not examples.
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.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.
git bisect. Halving the search space beats inspecting it.git bisect to find the commit, git blame to find the reason, and a profiler when the symptom is “slow” rather than “wrong”.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.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.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.
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.“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.
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.
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.
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.
cherry-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.git 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.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.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.
CMD or ENTRYPOINT so signals reach the process rather than a shell, and pin the base image by digest.CONCURRENTLY, will stop the site on a large table.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”.
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.Often asked as a story question — “tell me about an outage” — and scored on whether you separate mitigation from diagnosis, and blame from cause.
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.
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.
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.
// 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
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.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.iss 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.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.alg: 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.HttpOnly cookie in a browser, the keychain on a device, never local storage./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.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.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.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.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.
/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
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.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.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./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."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.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.../../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.?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.The vocabulary
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.
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.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.
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.
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.
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.
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.
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.
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.
The prompts, in the words they are usually asked
“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.
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.
| Dimension | Mid | Senior | Staff and beyond |
|---|---|---|---|
| Scope of work | A 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. |
| Ambiguity | Asks for the requirements. | Writes the requirements and gets them agreed. | Notices the requirement everyone assumed and challenges it. |
| Technical decisions | Follows the existing pattern. | Chooses between patterns and can defend the choice. | Decides which decisions need to be made now and which to defer. |
| Influence | Own work. | The team’s work; reviews, mentoring, standards. | Across teams, by writing and by making the good path the easy one. |
| Risk | Raises it. | Mitigates it and communicates the residue. | Trades it deliberately against time and cost, in public. |
| Failure | Reports it. | Owns it, fixes it, changes the default that allowed it. | Changes the system so the class of failure stops happening. |
| Communication | Clear 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.
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.
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.
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.
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.
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.