Interviewing well, section 3 of 5, 4h

The coding round where the spec keeps changing

By the end: Your first version survives the third change to the requirements, and you can say why you chose to refactor or to bolt on.

  • The shape of a levelled problem
  • Making version one extensible without over-building it
  • Naming your assumptions so they can be corrected
  • Refactor or bolt on
  • A worked rate limiter through four levels

New to interviewing well? Interviewing well is a separate skill from the engineering being tested, and it is made of specific, practisable habits: narrating your reasoning aloud instead of going quiet, asking what the constraints are before designing anything, checking your own answer before declaring it finished, and arriving with about eight short true stories ready, covering a conflict, a failure, an ambiguous brief and something you would now do differently. Why it matters

One problem, revealed in roughly four levels, each adding a requirement to the last, and you are not told the later ones in advance. This section is about what that format measures, which is whether your first version survives the third change rather than how clever it was. The working rule is a budget: at level one you buy extensibility that costs a line, such as an injected clock or a keyed state, and refuse anything that costs a structure. A rate limiter is taken through all four levels so you can see which early decisions paid and which cost.

One problem, four levels#

The dominant live-coding format at frontier labs, and increasingly elsewhere, is one problem in roughly four levels. Each level adds requirements to the last, or layers an extension onto a base problem. You are not told the later levels in advance. Some loops run this as a timed assessment, others as a live pairing session with an interviewer reading the next requirement out when you finish the previous one.

What is judged is whether your first design survives the third change, not asymptotic cleverness. A candidate who reaches level four with an ugly but coherent design scores above one who wrote an elegant level one and then rewrote it twice.

The tension is real, so say it out loud#

You are told to write extensible code and also not to over-engineer, and those instructions pull against each other. The resolution is not a principle, it is a budget: at level one you may buy extensibility that costs a line, and nothing that costs a structure.

  • Cheap and worth it: inject the clock instead of calling the system one, so the tests you write are not sleep-based.
  • Cheap and worth it: put the decision in one method rather than inline at the call sites, so there is one place to change.
  • Cheap and worth it: key your state by something, even if there is only one key today.
  • Expensive and not yet: a strategy interface, a plugin registry, an abstract base class with one implementation.
  • Expensive and not yet: a configuration object for parameters that have one value.

The test for whether an abstraction is cheap is whether removing it would make the code shorter than one screen of reading. If the answer is that it would need explaining, you bought a structure at level one.

Name your assumptions out loud#

Every underspecified problem has three or four assumptions that decide the design, and the interviewer knows which ones the later levels will break. Saying them aloud gives them the chance to correct you before you build on the wrong one. This is the highest-value thing you do in the first five minutes.

"I am going to assume single-threaded and single-process for
now, and I will keep the state behind one method so I can change
that later. I am also assuming keys are opaque strings and that
we do not need to evict them yet. Tell me if either of those is
wrong."
An interviewer who knows level three is concurrency will usually say something here. That is worth more than ten minutes of coding.

Level one: a rate limiter that works#

Take a rate limiter, one of the most commonly reported problems at this tier. Level one is usually the plain version: allow at most a fixed number of calls per key per window.

import time


class RateLimiter:
    """Fixed window: at most limit calls per window seconds, per key."""

    def __init__(self, limit: int, window: float, now=time.monotonic):
        self.limit = limit
        self.window = window
        self._now = now
        # key -> (window start, calls used in it)
        self._state: dict[str, tuple[float, int]] = {}

    def allow(self, key: str) -> bool:
        t = self._now()
        start, used = self._state.get(key, (t, 0))
        if t - start >= self.window:
            start, used = t, 0
        if used >= self.limit:
            self._state[key] = (start, used)
            return False
        self._state[key] = (start, used + 1)
        return True
Two cheap decisions: the clock is a parameter, and the state is keyed. Neither costs a structure, and both are what level two and level three need.

The injected clock also means you can test it without sleeping, which matters when you have forty minutes for four levels. A test at this point is not ceremony, it is the thing that tells you whether level three broke level one.

def test_fixed_window():
    clock = [0.0]
    limiter = RateLimiter(limit=2, window=10, now=lambda: clock[0])

    assert limiter.allow("alice")
    assert limiter.allow("alice")
    assert not limiter.allow("alice")
    assert limiter.allow("bob")  # keys are independent

    clock[0] = 10.0
    assert limiter.allow("alice")  # window rolled over

Level two: several limits at once#

The usual second level is more than one rule per key: five per second and a hundred per hour, or a per-endpoint limit on top of a per-user one. This is where hardcoded solutions die, because a single limit and window baked into the constructor has no room for a second rule.

It is also where the first genuine design question appears, and it is not about data structures. If a call passes the per-second rule and fails the per-hour rule, the per-second budget must not have been spent. Checking and consuming have to be separate passes.

import time
from dataclasses import dataclass


@dataclass(frozen=True)
class Rule:
    limit: int
    window: float


class RateLimiter:
    """All rules must pass. A rejected call consumes no budget."""

    def __init__(self, rules: list[Rule], now=time.monotonic):
        self.rules = rules
        self._now = now
        # (key, rule index) -> (window start, calls used in it)
        self._state: dict[tuple[str, int], tuple[float, int]] = {}

    def _window(self, key: str, i: int, rule: Rule, t: float):
        start, used = self._state.get((key, i), (t, 0))
        if t - start >= rule.window:
            return t, 0
        return start, used

    def allow(self, key: str) -> bool:
        t = self._now()
        pending = []
        for i, rule in enumerate(self.rules):
            start, used = self._window(key, i, rule, t)
            if used >= rule.limit:
                return False  # nothing written, so nothing was spent
            pending.append(((key, i), (start, used + 1)))
        for slot, value in pending:
            self._state[slot] = value
        return True
The level one shape survived. The state key gained a rule index, the window logic moved into one method, and the check became all-or-nothing.

Say the all-or-nothing decision out loud when you make it. It is the part an interviewer is watching for, and a candidate who spends the per-second budget on a call that was then rejected has a bug that only shows under a specific ordering.

Levels three and four#

Level three is usually one of two things. Either the caller needs to know how long to wait, which means allow returns a retry-after rather than a boolean and every caller changes; or several threads share the limiter, which means the read-check-write in allow is a race and needs a lock around the whole of it. Both are contained by the level two design, because there is exactly one method doing the deciding.

Level four is usually distribution: several processes sharing one budget. Nothing about the rules changes, but the dictionary has to become a store that another process can read, and the read-check-write has to become atomic in that store rather than in your process. The honest answer under time pressure is to extract the two lines that touch self._state into a small store interface, implement it in memory, and describe the backing implementation rather than write it.

Level 1   one limit per key
Level 2   several rules, all-or-nothing consumption
Level 3   retry-after, or thread safety
Level 4   shared across processes, atomic in the store

What changed each time: the state key, and the inside of one
method. What never changed: allow(key).

Refactor or bolt on#

Both are correct answers at different moments, and the failure is doing neither deliberately. Refactor when the new requirement contradicts a shape you chose, because bolting onto a contradiction produces a second code path that the next level will have to be added to twice. Bolt on when the new requirement is additive and the shape still holds.

  • Refactor when you would otherwise write a second branch that duplicates an existing one.
  • Refactor when the fix is a flag threaded through three methods.
  • Bolt on when the change is one new field and one new check.
  • Bolt on when you have under ten minutes left, and say that is why.

Announce it either way: 'this contradicts the single-window assumption, so I am going to spend three minutes restructuring rather than special-casing it' is a sentence that scores. Quietly rewriting for eight minutes while the interviewer watches is the same work with none of the credit.

The clock, as a habit#

Almost every levelled problem at this tier has a hidden dependency that later levels vary: the clock in a rate limiter, the storage in a key-value store, the transport in a batch processor, the random seed in a sampler. Passing it in at level one costs one parameter and buys you the next three levels.

If level two made you delete level one, you did not have a design

The point of the format is that the first version is a claim about what will vary. If the first requirement change forced a rewrite rather than an edit, the claim was wrong, and the fix is not to build more at level one but to keep the decision in one place. Equally, if you spent level one building an interface that level four never needed, you traded working code for a guess.

Where people go wrong

Building a beautiful abstraction at level one and running out of time, or building something so hardcoded that level two requires starting again. Both fail for the same reason: version one was written as if it were the final answer rather than the first of four.

If this comes up in an interview

How do you write the first version of a problem you know will get harder, without over-engineering it?

Spend only on extensibility that costs a line rather than a structure: inject the clock or the store, key the state even when there is one key, and keep the decision in one method. Refuse anything that needs explaining, such as a strategy interface with one implementation. Then say the assumptions out loud so the interviewer can correct the ones the later levels will break.

Then they push further

The next requirement contradicts your design. Do you refactor or special-case it?

Refactor when the alternative is a second branch duplicating an existing one, or a flag threaded through several methods, because the level after this will then have to be written twice. Special-case it when the change is additive, or when there is not enough time left, and say out loud that time is the reason.

How do you avoid breaking the earlier levels as you add to them?

Write a small test per level as you finish it, using an injected clock so it does not sleep. Re-running them after each change takes seconds and is the only thing that tells you the third requirement did not break the first.

What gets marked up. Strong answers give a concrete budget for what extensibility is worth buying early, name the hidden dependency they would inject, and treat refactoring as an announced decision rather than something done silently.

Design a rate limiter that supports several limits per user at once.

Keep one allow(key) entry point and key the state by key and rule. Check every rule before consuming any budget, so a call rejected by the hourly rule does not spend the per-second one. That all-or-nothing pass is the part that has a bug in most implementations.

Then they push further

Now make it safe under concurrent callers.

The read-check-write inside allow is the race, so the lock goes around the whole of it rather than around each dictionary access. If the state moves out of process, the same operation has to become atomic in the store instead, which is why it was worth keeping in one method.

You are twenty minutes in and realise the design will not take the next requirement. What do you do?

Say so, immediately, with the reason and a time estimate: what the requirement contradicts, what the restructure is, and roughly how long it takes. Then do it. Recognising it early and out loud is read as judgement; discovering it silently at the last level is read as not having noticed.

Build this: Give yourself forty minutes and a timer. Build the rate limiter above through all four levels, revealing each requirement to yourself only when the previous one passes its test. Note the minute at which each level started, and afterwards work out which single decision at level one cost you the most time later.

This is one section of Interviewing well, 20 hours across 5 sections. Read it in full to get the whole topic in order.

Stop 50 of 50 on the AI Engineer route

0 of 50 topics done · 0h behind you, 603h to go

That is the last stop on this route. What remains is the part no syllabus can do for you: build the projects, and apply.

What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.