Skip to content

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.

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.

A machine-coding delivery map shows requirements, domain model, API surface, implementation slice, tests, and polish leading to a small complete system.
Treat these as delivery obligations, not isolated phases: cross from requirements to a tested slice before perfecting any one layer.

Consider a common prompt:

Build an in-memory rate limiter that allows at most N requests 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:

  1. The request at the limit is allowed and the next one is rejected.
  2. Repeated rejection does not increase the stored count.
  3. Advancing the fake clock past the window permits a request again.
  4. A second user has an independent counter.
  5. 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_start and count;
  • token bucket stores tokens and last_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.