Senior Engineering Interview Handbook / Chapter 51
The Machine-Coding Round
A practical guide to machine-coding interviews, following one rate limiter from requirement slicing through a tested thin slice, a changed requirement, and a deliberate finish.
Preparing audio…
Audio edition
The Machine-Coding Round
Page tools
The first executable minute
Thirty-five minutes into a machine-coding round, two solutions may contain the same ideas. One has interfaces, factories, a diagram, and several empty methods. The other accepts one request, rejects the next one at the limit, and has a test that proves rejection did not corrupt its state.
Only one of them has begun to answer the prompt.
A machine-coding round enlarges the artifact from a function to a small running system. The interviewer can inspect its operations, state, errors, tests, and response to change. That breadth makes overbuilding tempting. The way through is controlled delivery: make the narrowest useful promise, get it running early, and let observed requirements—not imagined architecture—earn each new boundary.
Consider a common prompt:
Build an in-memory rate limiter that allows at most
Nrequests per user in each time window.
The rest of this chapter follows one attempt. The rate limiter is small enough to finish, but resistant enough to expose the choices that decide the round: what to ask, what to defer, where state belongs, which test matters, and when an abstraction finally becomes useful.
Cut the prompt down to a promise
Good clarification changes code. “Could this eventually work across several regions?” opens a large conversation but does not help with the first in-memory method. These questions do:
- What identifies a limit: user, token, IP address, or route?
- Is the first policy a fixed window, and how are window boundaries defined?
- What must the caller receive when a request is rejected?
- May this version assume one process and one thread?
- Can time be controlled in tests?
Suppose the answers give us a per-user, fixed-window limiter. It returns a
decision containing allowed, remaining, and retry_after_seconds. It runs
in one process, and its clock is injectable. Blank user IDs are invalid. The
interviewer wants allow(user_id) first; other policies may follow.
That is enough to state the first promise:
“I’ll implement a fixed-window
allow(user_id)with an injectable clock. The first slice will prove the limit boundary, retry information, and a reset in the next window. I’ll keep persistence, distribution, and other policies outside that slice.”
This answer reduces scope without making the system a toy. It retains the behavior that can fail: the boundary between the last allowed request and the first rejected one.
Before typing, write down three things:
operation: allow(user_id) -> LimitDecision
invariant: a counter never exceeds the limit in its active window
first proof: allow, allow, reject; then advance time and allow
The previous chapter used invariants to justify algorithms. Here the same discipline governs mutable state. The invariant gives implementation and tests a shared target.
Give the rule one owner
The first model can be almost plain:
LimitDecision
allowed
remaining
retry_after_seconds
reason
FixedWindowLimiter
limit
window_seconds
clock
windows_by_user
WindowCounter
window_start
count
FixedWindowLimiter owns the policy and the map. A WindowCounter records one
user’s active window. The decision object keeps rejection explicit; a bare
false cannot tell a caller whether the key was invalid or the quota was
exhausted.
There is no repository, factory, controller, or policy interface yet. None is needed to run the requested behavior. If the programming environment already has a lightweight application boundary, use it; do not recreate framework architecture inside the round.
One dependency does earn a boundary immediately: time. If the code reads the wall clock directly, the reset and retry tests must wait or race. Passing in a clock makes time an input:
Clock.now()
The choice is small, but consequential. It is the difference between claiming that expiration works and moving the test clock to prove it.
Chapter 52 develops the larger vocabulary of entities, values, aggregates, services, and policies. During this round, the useful question is narrower: which object must own the mutation so that a failed request cannot leave the system half changed?
Cross the whole system before widening it
Build one path from call to result. Do not finish “the model layer” while the public operation remains hypothetical.
allow(user_id):
if user_id is blank:
return rejected("invalid user")
now = clock.now()
counter = windows_by_user.get(user_id)
if counter is missing or now >= counter.window_start + window_seconds:
counter = WindowCounter(window_start=now, count=0)
windows_by_user[user_id] = counter
if counter.count >= limit:
retry_after = counter.window_start + window_seconds - now
return rejected("rate limit exceeded", retry_after)
counter.count += 1
return allowed(remaining=limit - counter.count)
This version uses the first request as the beginning of a user’s window. A
limiter aligned to wall-clock buckets would calculate window_start
differently. Neither choice should be hidden: boundary semantics determine
what happens around the minute mark.
Notice the order of mutation. Invalid input returns before a counter is
created. An exhausted counter returns before count changes. A new window is
installed before the accepted request increments it. Each return either
preserves the old valid state or leaves a new valid state.
Now run the behavior you promised:
limit = 2, window = 60 seconds
t=0 allow("u1") -> allowed, remaining=1
t=1 allow("u1") -> allowed, remaining=0
t=2 allow("u1") -> rejected, remaining=0, retry_after=58
t=60 allow("u1") -> allowed, remaining=1
At this point the system is incomplete but coherent. That is a much safer place to widen from than a collection of uncalled classes.
Let tests interrupt the design
Tests do not belong to the closing minutes. The first useful tests expose the boundary while it is still cheap to change:
- The request at the limit is allowed and the next one is rejected.
- Repeated rejection does not increase the stored count.
- Advancing the fake clock past the window permits a request again.
- A second user has an independent counter.
- A blank user ID creates no state.
If time permits only three, keep the limit boundary, no-mutation rejection, and reset. A happy-path test proves that syntax and wiring cooperate; these three attack the policy itself.
The second test often improves the design. If the test must reach into a private counter to learn whether rejection mutated state, add a public query only if callers genuinely need it. Otherwise, prove the property through behavior: reject twice at the same time and require the same remaining quota and retry interval, then advance to the next window and confirm the full quota is available. Tests should clarify the contract rather than force production code to expose its internals.
When a test fails, stop widening the system. A failing invariant is not a small defect beside the architecture; it is evidence that the architecture does not yet own its rule.
Make the changed requirement pay for the abstraction
Halfway through, the interviewer asks for token-bucket behavior so a user can accumulate a burst allowance while idle.
The fixed-window code should not be stretched with a mode flag and several branches. The state and transition rule have genuinely changed:
- fixed window stores
window_startandcount; - token bucket stores
tokensandlast_refill_time; - fixed window resets at a boundary;
- token bucket refills continuously or in stated increments.
The stable part is smaller:
RateLimiter.allow(user_id) -> LimitDecision
LimitDecision
Clock
Now a policy boundary has work to do:
RateLimitPolicy
allow(user_id) -> LimitDecision
FixedWindowPolicy
TokenBucketPolicy
Extract that boundary from working code, rerun the fixed-window tests, and then add token-bucket tests for refill, burst capacity, and exhaustion. The abstraction is credible because two policies demand different state and transitions while sharing a caller contract.
This is the moment interviewers often use to distinguish flexibility from speculation. “I can add token bucket without changing callers” is useful only if the original behavior still runs afterward. Preserve the evidence while you change the design.
Not every new request deserves a new type. Adding retry_after_seconds to an
existing decision may be a field and a calculation. Adding per-route limits
may change the key from user_id to (user_id, route). Adding a second
algorithm changes policy state and earns a boundary. Describe the pressure
before naming the pattern.
Keep production judgment outside the implementation budget
An in-memory limiter has an honest production boundary. Multiple processes cannot enforce one global quota through separate maps. A shared store or shard owner would need an atomic update. Clock skew affects refill and window semantics. Unbounded user keys need expiration or eviction. Fail-open versus fail-closed behavior becomes a product and reliability decision.
Those are good closing observations and poor opening tasks unless the prompt explicitly includes them. The senior move is to show where the local contract would meet production machinery:
“The policy transition is the atomic boundary. A distributed version would move that state to an owner capable of compare-and-update or a single atomic script. We would also need explicit eviction and failure semantics. I kept those out of the in-memory implementation so the round’s requested behavior is complete.”
This answer neither dismisses scale nor pretends to build it. It carries the invariant to the edge of the artifact and identifies what a larger system would have to preserve.
Finish from a working state
The final minutes are not spare implementation time. Stop adding features early enough to do four things:
- rerun the complete test suite or demonstration;
- remove dead branches and repair misleading names;
- state the assumptions that materially shape behavior;
- identify one known limitation and the next justified extension.
Do not begin a broad refactor unless a test or requested change requires it. Code that worked five minutes ago is not evidence for code left halfway through a cleanup.
A compact close for this attempt would be:
“The fixed-window limiter handles independent users, explicit rejection, deterministic reset, and invalid keys. The tests cover the quota boundary, unchanged state after rejection, and the next window. The clock and decision shape remain stable across the token-bucket extension. Persistence and cross-process atomicity are outside this implementation; the policy update is where they would attach.”
The close is strong because every claim points to observable code or a named boundary.
Rehearse the change, not the speech
Use a 45-minute practice round with a prompt such as a booking service, inventory reservation, parking lot, cache, expense ledger, or game engine. Spend the opening minutes naming the callable operation, the first rejection, and the state it must leave unchanged. Get one slice running, then draw a random change:
- reservations now expire;
- the cache adds LRU eviction;
- bookings can recur;
- a parking spot may be taken out of service;
- expenses add percentage splits;
- a game adds a second win condition.
Before editing, say which rule changed, which state owns it, and what caller contract should remain stable. Make the smallest change that proves the answer, rerun the earlier tests, and stop with enough time to explain what is still absent.
Afterward, inspect the artifact rather than assigning yourself a broad score:
- When did the first end-to-end behavior run?
- Which test could have disproved the core invariant?
- Did any failed command leave partial state?
- Which abstraction existed before the requirement that justified it?
- Was the final version running when the clock ended?
The weak point in those answers determines the next drill. If executable code arrived late, practice thinner slices. If change scattered through the code, practice state ownership. If rejection was hard to test, improve result shapes and dependency control.
Field reference
Before code
callable operation and result
assumptions that change implementation
core invariant and first rejection
one explicit non-goal
First slice
cross the system from call to result
keep mutable truth under one owner
control time, IDs, or randomness when behavior depends on them
run success, boundary, rejection, and reset as applicable
When the prompt changes
name the rule and state that changed
preserve the stable caller contract
extract only the boundary the new variation earns
rerun old evidence before adding new evidence
Before time expires
stop widening
rerun everything
remove dead work and fix misleading names
state assumptions, production boundary, and next extension
The round is not won by showing how much architecture you can imagine. It is won by making judgment executable: a small promise kept, a rule protected, a change absorbed, and a working system still in your hands when time is called.
Related reading
Continue reading
Full table of contents