What changes about coding rounds at staff level
What it is
Staff and TL loops still have coding rounds. Google, Meta, Stripe, Databricks, Uber and most AI labs all keep at least one, and candidates who assume seniority exempts them from it are the ones who fail it. The bar is not harder problems. It is cleaner code, better tests, and no flailing. A staff candidate and a senior candidate are usually given the same question, and the separation happens in the five minutes before any algorithm is written and the five minutes after it works.
Concretely, five behaviours separate the two, and each of them is trainable independently of your algorithm knowledge:
- Talk about the API before the algorithm. Name the types, name the contract.
- Write tests unprompted. Even three assertions. This is a differentiator at every level above senior.
- State complexity before coding, then verify after.
- Handle the "make it production" follow-up: what breaks at 100 times the input, where does it go concurrent, what is the failure mode.
- Do not over-abstract. Staff candidates lose points for building a factory when a function was asked for.
Don't be confused: "staff coding rounds are easier" is a misreading of a true observation. The problems are often no harder, and are sometimes easier, than what a new grad gets. What changes is the rubric. A senior candidate is graded mostly on whether the algorithm is correct and reasonably efficient. A staff candidate is graded on that plus judgement: did you clarify the contract, did you test, did you know your own complexity, did you recognise the production failure mode, did you resist over-engineering. You can solve the problem perfectly and still be rated below the bar for skipping all five, and this is the most common way strong engineers fail these loops.
The problem it solves
The coding round exists at this level for a reason that is not "can you code". It is a sampling of how you will behave when you write code that other people depend on. Every one of the five behaviours maps to something a team actually needs: contracts before implementations, tests as a default rather than an afterthought, honest cost estimates, awareness of scale limits, and restraint about abstraction. The interviewer is extrapolating from forty minutes to two years.
That framing is also what makes the round trainable. You cannot reliably become better at inventing algorithms under pressure in eight weeks. You can absolutely become someone who always states the contract first, always writes three assertions, and always volunteers the complexity, and those are worth more marks than the one hard problem you might have missed.
Mechanics
The five behaviours on a deliberately small problem, so nothing is hidden by the algorithm. The question, as an interviewer would give it: "Given a list of log lines, return the k endpoints with the most requests."
"""What a staff-level coding answer looks like, on a deliberately small problem.
Runnable: `python3 staff_coding_style.py`. The problem is trivial on purpose.
What is being demonstrated is the ORDER of work and the surrounding behaviour,
because at staff level those are what the round measures.
The problem, as an interviewer would state it:
"Given a list of log lines, return the k endpoints with the most requests."
"""
import heapq
import random
from collections import Counter
from dataclasses import dataclass
# --- Step 1: the contract, before any algorithm ------------------------------
# Naming the types first is the single cheapest way to surface the ambiguities.
# Writing these four lines out loud produced three questions that changed the
# implementation, which is the point.
@dataclass(frozen=True)
class LogLine:
endpoint: str
status: int
latency_ms: float
# Questions the contract surfaced, and the answers assumed here:
# 1. Ties at the k-th position: broken by endpoint name ascending, so the
# result is deterministic. An unstable answer is untestable.
# 2. Do failed requests count? Assumed yes, since the question said requests,
# not successful requests. Flagged rather than silently decided.
# 3. k larger than the number of distinct endpoints: return all of them,
# rather than raising. Padding with nulls would push the problem onto the
# caller.
def top_endpoints(lines, k):
"""The k endpoints with the most requests, most frequent first.
Ties are broken by endpoint name ascending. Returns fewer than k entries if
there are fewer than k distinct endpoints.
Time: O(n + m log k) for n lines and m distinct endpoints.
Space: O(m) for the counter, O(k) for the heap.
"""
if k <= 0:
return []
counts = Counter(line.endpoint for line in lines)
# Negate the name so that within the min-heap, the endpoint that loses a tie
# is the one that sorts LATER alphabetically. Strings cannot be negated, so
# rank by count and resolve the final order in the sort below.
return heapq.nsmallest(k, counts.items(), key=lambda kv: (-kv[1], kv[0]))
# --- Step 2: tests, written before being asked -------------------------------
# Three assertions was the bar named in the source. These are five, and they are
# chosen to be the cases most likely to be wrong rather than the cases easiest
# to write: empty, ties, k over-large, k non-positive, and a real distribution.
def test_top_endpoints():
def L(ep):
return LogLine(ep, 200, 1.0)
assert top_endpoints([], 3) == [], "empty input"
assert top_endpoints([L("/a")], 0) == [], "k of zero"
assert top_endpoints([L("/a")], -1) == [], "negative k"
# k larger than the distinct count returns everything, not a padded list.
assert top_endpoints([L("/a"), L("/b")], 5) == [("/a", 1), ("/b", 1)], "k too large"
# Ties resolve alphabetically, so this is deterministic across runs.
tied = [L("/z"), L("/z"), L("/a"), L("/a"), L("/m")]
assert top_endpoints(tied, 2) == [("/a", 2), ("/z", 2)], "tie broken by name"
real = [L("/search")] * 5 + [L("/health")] * 3 + [L("/index")] * 1
assert top_endpoints(real, 2) == [("/search", 5), ("/health", 3)], "ordinary case"
return 6
# --- Step 3: the "make it production" follow-up ------------------------------
# The interviewer asks: what breaks at 100x input? Answering with code beats
# answering with adjectives.
class StreamingTopK:
"""The same question over a stream that does not fit in memory.
What changed and why:
- Counter over all endpoints is O(m) memory and m is unbounded when the
endpoint set includes path parameters (/user/12345). That is the failure
at 100x, and it is a memory failure, not a speed one.
- So: bound the tracked set. This is the Space-Saving algorithm, which
keeps `capacity` counters and, on overflow, evicts the current minimum
and gives the newcomer that count plus one. The result is approximate,
with a bounded overestimate, and it never exceeds `capacity` entries.
- The honest caveat: it can miss a true heavy hitter that arrives late,
and it over-counts. Both are acceptable for a top-endpoints dashboard
and are not acceptable for billing. Say which one you are building.
"""
def __init__(self, capacity=100):
self.capacity = capacity
self.counts = {}
def add(self, endpoint):
if endpoint in self.counts:
self.counts[endpoint] += 1
elif len(self.counts) < self.capacity:
self.counts[endpoint] = 1
else:
victim = min(self.counts, key=self.counts.get)
self.counts[endpoint] = self.counts.pop(victim) + 1
def top(self, k):
return heapq.nsmallest(k, self.counts.items(), key=lambda kv: (-kv[1], kv[0]))
# --- Step 4: what NOT to do --------------------------------------------------
# The over-abstraction failure, shown rather than described. Everything below is
# a faithful reimplementation of top_endpoints, in 4 classes instead of 4 lines.
# It was asked for as a function.
class AbstractCounterStrategy:
def count(self, lines):
raise NotImplementedError
class EndpointCounterStrategy(AbstractCounterStrategy):
def count(self, lines):
return Counter(line.endpoint for line in lines)
class TopKSelectorFactory:
@staticmethod
def create(strategy):
return TopKSelector(strategy)
class TopKSelector:
def __init__(self, strategy):
self._strategy = strategy
def select(self, lines, k):
counts = self._strategy.count(lines)
return heapq.nsmallest(k, counts.items(), key=lambda kv: (-kv[1], kv[0]))
if __name__ == "__main__":
passed = test_top_endpoints()
print(f"tests: {passed} assertions passed")
sample = ([LogLine("/search", 200, 12.0)] * 5
+ [LogLine("/health", 200, 0.4)] * 3
+ [LogLine("/index", 500, 90.0)] * 2)
print("top_endpoints(k=2) :", top_endpoints(sample, 2))
print("top_endpoints(k=99) :", top_endpoints(sample, 99), "<- fewer than k, not padded")
# The streaming version on a skewed stream with an unbounded key space, which
# is the input that breaks the exact version's memory.
rng = random.Random(7)
stream = StreamingTopK(capacity=8)
exact = Counter()
for _ in range(20000):
if rng.random() < 0.5:
ep = rng.choice(["/search", "/health", "/index"])
else:
ep = f"/user/{rng.randrange(100000)}" # unbounded key space
stream.add(ep)
exact[ep] += 1
print()
print("distinct endpoints seen :", len(exact))
print("exact counter entries :", len(exact), "<- unbounded memory")
print("streaming counter entries:", len(stream.counts), "<- bounded at capacity")
print("streaming top 3 :", stream.top(3))
print("exact top 3 :", heapq.nsmallest(
3, exact.items(), key=lambda kv: (-kv[1], kv[0])))
over = TopKSelectorFactory.create(EndpointCounterStrategy()).select(sample, 2)
print()
print("the over-abstracted version returns:", over)
print("identical output, 4 classes instead of 4 lines, and a function was asked for")
Read that file in the order the work happens.
The contract comes first, and it is where the questions come from. Writing the
LogLine dataclass and the docstring surfaced three ambiguities that the problem statement
did not settle: how ties are broken, whether failed requests count, and what happens when
k exceeds the number of distinct endpoints. Each of those is a question worth asking out
loud, and none of them occurs to you until you try to name the types. That is the actual
mechanism behind "talk about the API first": it is not a ritual, it is a generator of the
clarifying questions interviewers are waiting to hear. The comment block records both the
question and the assumption taken, which is what you would do in a code review.
The complexity line is in the docstring, written before the body. O(n + m log k) for
$n$ lines and $m$ distinct endpoints. Stating it before coding commits you, and then
verifying it afterwards catches the case where your implementation is not the algorithm you
described. Writing it in the docstring rather than saying it aloud means it survives into
the artifact.
The tests are chosen for the cases most likely to be wrong, not the cases easiest to
write. Empty input, k = 0, negative k, k larger than the distinct count, and a tie.
Notice that four of the six assertions are boundary conditions and only one is an ordinary
case. A test suite that exercises the happy path three times demonstrates nothing; the
selection of cases is the signal, not the count.
StreamingTopK is the production follow-up answered in code. The exact version's
failure at scale is not speed, it is memory: a Counter grows with the number of distinct
endpoints, and once endpoints include path parameters like /user/12345 that set is
unbounded. The fix is the Space-Saving algorithm, which keeps a fixed number of counters and
on overflow evicts the current minimum, giving the newcomer that count plus one. It is
approximate with a bounded overestimate, and the docstring states exactly what that buys and
costs.
The last section is the anti-pattern, shown rather than described. Four classes, an
abstract base, a factory and a strategy, producing byte-identical output to a four-line
function that was what the interviewer asked for. It is in the file because reading it next
to top_endpoints makes the point more sharply than any advice about over-engineering.
Worked example
tests: 6 assertions passed
top_endpoints(k=2) : [('/search', 5), ('/health', 3)]
top_endpoints(k=99) : [('/search', 5), ('/health', 3), ('/index', 2)] <- fewer than k, not padded
distinct endpoints seen : 9518
exact counter entries : 9518 <- unbounded memory
streaming counter entries: 8 <- bounded at capacity
streaming top 3 : [('/health', 3363), ('/index', 3351), ('/search', 3315)]
exact top 3 : [('/health', 3362), ('/index', 3351), ('/search', 3315)]
the over-abstracted version returns: [('/search', 5), ('/health', 3)]
identical output, 4 classes instead of 4 lines, and a function was asked for
The three numbers that make the production argument are 9518, 9518 and 8. Twenty thousand events over an unbounded key space produced 9,518 distinct endpoints. The exact counter holds all of them and would hold ten million on a real day. The streaming counter holds 8, which is the capacity it was given, permanently, regardless of stream length. That is the difference between an approach that works on the interviewer's sample and one that works on Tuesday's traffic.
Look at /health: 3363 streaming against 3362 exact. The approximation is off by
exactly one, in the upward direction, and that is not luck. Space-Saving's guarantee is
that it never undercounts, because an evicted victim's count is inherited by the newcomer.
So the reported count is an upper bound on the true count, and the error is bounded by the
number of evictions. Being able to say "it overestimates, never underestimates, and here
is why" is what makes it an engineering choice rather than a hopeful one, and it leads
directly to the sentence that matters: this is fine for a top-endpoints dashboard and is not
fine for billing.
The k=99 line exists to make an assumption visible in the output. Returning three results
when 99 were asked for is a decision, and the alternatives (raise, or pad with nulls) are
defensible too. The failure is not picking wrong, it is picking silently.
Production evidence
The five behaviours are visible in how strong teams actually work, which is why they are graded. Contract-first design is the premise of interface definition languages: protobuf and OpenAPI schemas exist so that the contract is written, reviewed and versioned before any implementation. When an interviewer asks you to name types first, they are asking for the thing your team already requires of a new service.
Space-Saving and its relatives are deployed, not academic. The heavy-hitters problem is
solved in production by exactly this family of bounded-memory sketches: Space-Saving,
Count-Min Sketch, and the topk structures in Redis's RedisBloom module. Any system
reporting "top talkers", "top endpoints" or "top offending IPs" over high-volume traffic is
running one, because the exact version does not fit.
Tests-with-the-first-commit is a norm at the companies running these loops. Google's public engineering practices documentation makes tests part of what a code review is expected to check, and the Beyoncé rule ("if you liked it, you should have put a test on it") is from Google's own Software Engineering at Google. When an interviewer notices you wrote assertions without being asked, they are checking for a habit their codebase depends on.
The over-abstraction failure has a name and a literature. "You aren't gonna need it" and "premature abstraction is worse than premature optimisation" are Extreme Programming maxims that predate the current interview format by two decades, and the reason they persist is that the cost of a wrong abstraction is paid by everyone who later has to work around it, while the cost of a duplicated function is paid once.
The debate
Should you write tests before the interviewer asks, given the clock? There is a real cost: three assertions take two or three minutes of a 35 to 45 minute round. The argument against is that a candidate who runs out of time with a tested partial solution scores worse than one who finishes untested. My position: write the tests, and write them after a working solution rather than before. The reason is that the marginal value of the test is highest once code exists (it can find a real bug live, which is a dramatic positive signal) and the risk of the clock is lowest at that point because the algorithm is already done. Test-first is better engineering and worse interview strategy, and it is worth being honest about that rather than pretending they align. If you are running badly out of time, say "here are the three cases I would assert on, in order of what I think is most likely broken" and name them. That captures most of the credit for none of the clock.
How much clarification is too much? Candidates over-corrected by advice to "ask clarifying questions" now spend eight minutes interrogating a twenty-minute problem, and it reads as stalling. The useful discipline: ask only questions whose answers would change your code, and say why each one matters. "Are ties broken by name or is any order fine? It changes whether the output is deterministic and therefore whether I can test it." That sentence is one question, ten seconds, and it demonstrates the reasoning. Three of those is plenty. Questions whose answer would not change anything are noise.
When is abstraction actually correct in a coding round? When the problem statement
itself contains the variation. If the interviewer says "and later we will want to rank by
latency instead of count", a parameter or a key function is warranted, and refusing to
abstract at that point is its own failure. The rule is not "never abstract", it is
abstract in response to a stated requirement, never in anticipation of an imagined one.
The key=lambda kv: (-kv[1], kv[0]) in top_endpoints is exactly the right amount: it is
one parameter's worth of flexibility, in the place the requirement pointed at.
Is the volume target realistic? The source's guidance is roughly 70 to 90 problems total, at a distribution of about 20 percent easy, 65 percent medium and 15 percent hard, rather than the 400-problem grind. I would defend that number as correct for this level and add the reason: at staff level, the marginal return on problem 200 is close to zero, because the round is not testing pattern recall at that depth, while the marginal return on recording yourself solving problem 30 and watching it back is large. Depth over count is not a comfort; it is what the rubric implies.
On which 70 to 90: a curated, pattern-organised list beats picking problems by difficulty tag, because the point is coverage of the twelve patterns rather than volume. NeetCode 150 is the list I would use, since it is explicitly grouped by pattern and its groups map almost one to one onto the six pages that follow, which makes gaps visible: if you have done no union-find problems, the list shows it, whereas a difficulty-sorted queue hides it. Work one pattern to the point where the recognition is automatic before moving on, rather than sampling across patterns, because the failure mode in a real round is not being unable to implement a pattern you recognise; it is not recognising which pattern the problem is.
Follow-up Q&A
What exactly is the drill format? Timed, 35 minutes, out loud, in a plain editor with no autocomplete and no language server. Record yourself once a week and watch the recording. Every element of that is doing work: the timer builds the pacing instinct, speaking out loud trains the narration you will be graded on, the plain editor removes the completion crutch you will not have in a shared doc, and the recording is the only way to see your own flailing, which is invisible from the inside. It is unpleasant and it is the single highest return practice in the whole track.
How do I narrate without either going silent or babbling? Narrate decisions, not keystrokes. "I am choosing a min-heap so eviction is at the top" is a decision. "Now I type a for loop" is a keystroke. When you need to think silently, say so: "give me twenty seconds to think about the state definition" is a completely acceptable sentence and is far better than either dead air or filler. Interviewers are taking notes during your silences anyway.
What do I do when I am genuinely stuck? Say what you have ruled out and why, then state the simplest thing that would work. "Brute force here is $O(n^2)$ and I can write it in two minutes. I think there is an $O(n \log n)$ using a sorted structure but I have not closed it. Shall I write the brute force first so we have something correct, and then optimise?" That is a staff-level response to being stuck: it is honest, it makes progress, it manages the clock, and it hands the interviewer a decision they are usually happy to make. Silent flailing for eight minutes is what fails.
The interviewer asks "how would you make this concurrent?" What are they looking for?
Where the shared mutable state is, and what you would do about it. In top_endpoints, the
counting phase is embarrassingly parallel (partition the lines, count independently, merge
the counters, because addition is associative and commutative) and the top-k selection is
cheap and serial. That decomposition, map-reduce with a named reason why the merge is
valid, is the answer. The wrong answer is reaching for a lock around a shared counter,
which serialises the parallel part. If asked about the streaming version, note that
Space-Saving's merge is not exact, which is a genuine and interesting complication.
How do I answer "what breaks at 100 times the input?" if I have not thought about it?
Work through the resources in order: memory, then time, then I/O, then coordination. Ask of
each: what grows? In top_endpoints, memory grows with distinct endpoints and time grows
with lines, so at 100 times, if the line count grows but the endpoint set does not, you are
fine; if the endpoint set is unbounded, memory is the wall. Naming which of the two grew
is the whole answer, and that checklist works on any problem.
Do these expectations differ between a staff IC loop and a TL loop? The coding bar is essentially identical; both want the five behaviours. The difference appears in the follow-up discussion, where a TL loop is more likely to push on "how would you get a team to maintain this" and "how would you review this PR", and a staff IC loop is more likely to push on the scale and correctness limits. Prepare the same way and expect the follow-up to tilt.
Common misconceptions
"At staff level they care about design, not coding, so I can skip practice." The round exists, it is scored, and it is the most common single point of failure for experienced candidates precisely because they under-prepare it relative to the design rounds. Light daily practice is maintenance; skipping it entirely is not a strategy.
"Writing tests will make me run out of time." Three assertions is two minutes. The candidates who run out of time do so because they started coding before understanding the problem, not because they tested.
"Stating complexity is a formality since the interviewer can see the code." They can, and they are checking whether you can. Stating a bound before coding and then discovering your implementation does not match it is a normal, recoverable event that reflects well on you if you catch it yourself. Never stating one means the question gets asked, and then you are answering rather than volunteering.
"Clean code means extracting helper functions." Sometimes. In a 35-minute round, extracting a helper used once usually costs clarity, because the reader now has to jump around a small file. Clean here means good names, no dead code, handled edge cases, and a correct docstring. It does not mean structure for its own sake, which is the over-abstraction failure in a different costume.
Interview delivery note
The opening thirty seconds are worth rehearsing until they are automatic, because they set the frame for everything after. Something close to: "Before I code, let me state the contract. Input is a list of log lines, each with an endpoint, a status and a latency. Output is the k endpoints with the highest request count, most frequent first. Two things the statement leaves open: how ties break, and what happens if k exceeds the number of distinct endpoints. I will assume ties break by name so the output is deterministic and testable, and that we return fewer than k rather than padding. Complexity target is O(n) to count plus O(m log k) to select. Sound right?"
That is one breath, it contains the contract, two clarifying questions with their justifications, an assumption stated rather than hidden, and a complexity commitment. Almost nobody does it, and every interviewer notices.
The senior-to-staff separator across this whole page is the direction of the information flow. A senior candidate answers well: asked about complexity, they give it; asked what breaks at scale, they reason it out. A staff candidate volunteers: the contract, the assumption, the complexity, the test cases and the scaling limit all arrive without being requested, in the order a colleague would want them. The content can be identical. What differs is who had to ask, and that is the thing the interviewer writes down.
Further reading
- Titus Winters, Tom Manshreck and Hyrum Wright. Software Engineering at Google. O'Reilly, 2020. The testing culture chapters, for why "tests unprompted" is a hiring signal rather than a preference.
- Ahmed Metwally, Divyakant Agrawal and Amr El Abbadi. "Efficient Computation of Frequent and Top-k Elements in Data Streams." ICDT 2005. The Space-Saving algorithm used above, including the proof that it never undercounts.
- Google's Code Review Developer Guide, for what "clean code" is actually graded against inside one of the companies running these loops.
- Steve McConnell. Code Complete, 2nd ed. Chapter 5, on the cost of abstraction chosen before the requirement that justifies it.