Agent-written retry loops often treat a network timeout as proof that a POST never committed. They then fire the same non-idempotent grant again, which duplicates durable side effects in production. Interview packets that only score exponential backoff miss the real defect sitting under the loop. This article presents a frozen take-home task, a rubric, a repaired sketch, and repeated failure modes.
The packet fits teams that already ask candidates to review model-generated TypeScript rather than greenfield services. It also keeps the time box inside a single worker file so the invariant cannot hide in framework noise. Hiring loops that still use CRUD toys will not extract the same signal from this fixture.
Why this defect survives code review
Timeouts occupy a gray zone that completion models compress into one tidy story about failure. In seat-grant APIs the server may persist a row before response bytes leave the process. A second POST then creates another seat, another webhook, and another support ticket to reconcile. Backoff, jitter, and max-attempt counters improve load behavior, yet they leave that commit ambiguity untouched.
Reviewers who stop at exponential backoff are only scoring the prompt the agent already satisfied. The production invariant is narrower than “keep trying until the network looks healthy again.” Each requestId must grant at most one seat, including after resets, 409 replays, and dropped 201 bodies.
The take-home prompt
Give candidates the following instructions as a frozen packet with no extra ticket history. Additional product docs turn this round into a context-window test rather than an idempotency review. Keep the prompt text identical across candidates so calibration stays possible.
Prompt (paste as-is)
Review the TypeScript worker below. It consumes
seat.requestedevents and callsPOST /v1/seatsto grant a workspace seat. Production has seen duplicate seats for a singlerequestIdafter gateway timeouts. Write a review, not a rewrite-from-scratch. List defects by severity, name the invariant that broke, and propose a minimal patch. Do not invent a queue product. Do not assume at-least-once delivery is a bug by itself.
Constraints for the candidate
- Time box: forty-five minutes of review and fifteen minutes of patch sketch.
- Runtime: TypeScript on Node 20, with no framework requirement in the fixture.
- Deliverable: markdown review plus a unified diff against the supplied worker.
- Out of scope: authentication redesign, multi-region consensus, and load-test harnesses.
Fixture: agent-written worker
Label the following as an unexecuted interview fixture, not as canonical production code. Candidates who only reformat the file have not started the review. The unused requestId field is intentional bait for the rubric.
// fixture: seat-grant-worker.ts (agent-written, do not treat as canonical)
import { setTimeout as delay } from "node:timers/promises";
type SeatEvent = {
requestId: string;
workspaceId: string;
userId: string;
};
type GrantBody = {
workspaceId: string;
userId: string;
role: "member";
};
async function postGrant(body: GrantBody): Promise<{ id: string }> {
const res = await fetch("https://api.example.internal/v1/seats", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`grant_failed:${res.status}`);
}
return res.json() as Promise<{ id: string }>;
}
export async function handleSeatRequested(event: SeatEvent): Promise<void> {
const body: GrantBody = {
workspaceId: event.workspaceId,
userId: event.userId,
role: "member",
};
let attempt = 0;
const maxAttempts = 5;
while (attempt < maxAttempts) {
try {
const granted = await postGrant(body);
console.log("granted", granted.id, event.requestId);
return;
} catch (err) {
attempt += 1;
const backoffMs = 100 * 2 ** attempt;
await delay(backoffMs);
}
}
throw new Error(`exhausted_retries:${event.requestId}`);
}
A local harness can be bootstrapped with ordinary Node tooling before any model pass begins. The commands below only establish a review sandbox for the fixture and the later fake-fetch tests.
mkdir -p takehome-seat-grant && cd takehome-seat-grant
npm init -y
npm install -D typescript tsx @types/node
npx tsc --init --rootDir . --strict --module nodenext --moduleResolution nodenext
Rubric
Score the review, not the candidate's prose style, and lock the matrix before the first packet goes out. Passing score is six of ten, with a non-zero mark on timeout semantics and on idempotency. A perfect backoff implementation with those two columns at zero still fails.
| Signal | Weak (0) | Adequate (1) | Strong (2) |
|---|---|---|---|
| Timeout semantics | Calls timeout a hard failure with no commit risk | Mentions possible success without a protocol | States that a dropped response can follow a durable write |
| Idempotency | Deletes retries or only adds sleep
|
Suggests “check first” without a key | Requires an Idempotency-Key or server dedupe on requestId
|
| Error classes | Retries every thrown error | Skips 4xx in prose, still retries in code | Splits 409 replay, 429/503 retry, and 400/401/403 no-retry |
| Observability | Relies on console.log alone |
Asks for a counter | Ties requestId, attempt, and outcome into one structured event |
| Patch size | Rewrites the worker into a framework | Adds helpers without a contract | Minimal diff: header, 409 handling, no silent empty catch
|
What a strong review says
A strong review names the invariant in one sentence and then refuses to treat the catch block as a classifier. Each requestId grants at most one seat, even when the client observes a timeout, a connection reset, or a 409 replay. The present catch path erases error classes, because fetch failures, 4xx bodies, and 5xx bodies collapse into one sleep loop.
The review should also note that requestId already exists on the event and never reaches the HTTP boundary. That unused field is the cheapest idempotency key available in the fixture. Leaving it in logs only is a design smell rather than a naming nit.
Sample patch sketch
Label this patch as a proposal for the interview key, not as load-tested production code. Network TypeError values stay retryable only because the server is assumed to honor the key. Candidates who restore retries without the header have not closed the incident.
async function postGrant(
body: GrantBody,
idempotencyKey: string
): Promise<{ id: string; replayed: boolean }> {
const res = await fetch("https://api.example.internal/v1/seats", {
method: "POST",
headers: {
"content-type": "application/json",
"idempotency-key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (res.status === 409) {
const existing = (await res.json()) as { id: string };
return { id: existing.id, replayed: true };
}
if (res.status === 429 || res.status === 503) {
throw Object.assign(new Error(`retryable:${res.status}`), { retryable: true });
}
if (!res.ok) {
throw Object.assign(new Error(`fatal:${res.status}`), { retryable: false });
}
const granted = (await res.json()) as { id: string };
return { id: granted.id, replayed: false };
}
function isRetryable(err: unknown): boolean {
if (err instanceof TypeError) return true;
if (err instanceof Error && err.name === "AbortError") return true;
return (
typeof err === "object" &&
err !== null &&
"retryable" in err &&
(err as { retryable: boolean }).retryable === true
);
}
export async function handleSeatRequested(event: SeatEvent): Promise<void> {
const body: GrantBody = {
workspaceId: event.workspaceId,
userId: event.userId,
role: "member",
};
let attempt = 0;
const maxAttempts = 5;
while (attempt < maxAttempts) {
try {
const granted = await postGrant(body, event.requestId);
console.log("granted", {
seatId: granted.id,
requestId: event.requestId,
replayed: granted.replayed,
attempt,
});
return;
} catch (err) {
if (!isRetryable(err)) {
throw err;
}
attempt += 1;
if (attempt >= maxAttempts) {
throw new Error(`exhausted_retries:${event.requestId}`);
}
const backoffMs = 100 * 2 ** attempt;
await delay(backoffMs);
}
}
}
Companion test plan
Run these cases against a fake fetch, not against a live billing or identity cluster. A reviewer who adds tests only for backoff timing has optimized the least important axis. The file name below is a suggestion, not a required harness layout.
// proposal: seat-grant-worker.test.ts (unexecuted interview key)
import assert from "node:assert/strict";
import { test } from "node:test";
import { handleSeatRequested } from "./seat-grant-worker.ts";
function mockFetch(queue: Array<Response | Error>): typeof fetch {
return (async () => {
const next = queue.shift();
if (!next) throw new Error("unexpected_fetch");
if (next instanceof Error) throw next;
return next;
}) as typeof fetch;
}
test("400 is not retried", async () => {
const calls: number[] = [];
globalThis.fetch = mockFetch([
new Response("{}", { status: 400 }),
]);
globalThis.fetch = new Proxy(globalThis.fetch, {
apply(target, thisArg, args) {
calls.push(1);
return Reflect.apply(target, thisArg, args);
},
});
await assert.rejects(() =>
handleSeatRequested({
requestId: "req_1",
workspaceId: "ws_1",
userId: "user_1",
})
);
assert.equal(calls.length, 1);
});
Numbered outcomes that a complete key should still cover:
- First POST returns 201: the worker exits after one attempt and logs
replayed: false. - First POST drops with
TypeError, second POST returns 409 with the sameid: the worker exits without a third call. - First POST returns 400: the worker does not retry and does not sleep.
- First POST returns 503, then 201: the worker retries once and records both attempts under the same
requestId. - Five retryable failures: the worker throws
exhausted_retriesand never swallows the error.
npx tsx --test seat-grant-worker.test.ts
Common failure modes
Agents and candidates fail this packet in overlapping ways, which keeps calibration meetings short when the answer key lists both. The lists below are review notes, not extra product requirements for the take-home.
Agent failure modes in the fixture and in later regenerations
- Retrying POST without a key because the prompt mentioned transient errors.
- Mapping every
!res.okpath, including 400 and 401, onto the same sleep loop. - Logging
granted.idwhile droppingrequestId, which makes duplicate seats un-joinable later. - Treating 409 as a hard failure instead of a successful replay of the original grant.
- Inventing a process-local map of
requestIdvalues that disappears on restart.
Human reviewer failure modes
- Deleting the loop and declaring at-most-once delivery, which the event bus does not provide.
- Demanding a full saga framework for a single resource grant inside a forty-five minute box.
- Accepting GET-then-POST without a unique constraint, which still races under overlap.
- Spending the time box on naming, import order, and comment tone.
- Rewriting the worker in another language the team does not run in production.
Running the packet without a private review cluster
Teams that already collect model-generated diffs can run the same packet through a second model pass and compare both reviews against the rubric. MonkeyCode's free model access and free server option are enough for that comparison when a dedicated review box is not worth provisioning for a forty-five minute exercise.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The useful output remains the scored markdown, not a claim that any hosted runtime replaces the table. Candidates who paste the fixture into a model and accept the first rewrite should be scored as they would in a hallway review. The packet tests judgment under a known invariant, not fluency at prompting. Interviewers who already freeze packets like this can run that second pass on the free server option and then mark both reviews with the matrix above.
Limitations and who should skip this packet
This packet does not certify distributed-systems expertise, frontend craft, or whiteboard system design. It also does not measure product negotiation with a PM. Skip it when the role will not read agent-written TypeScript, when the API already has mandatory idempotency middleware with tests, or when the hiring bar is chalkboard algorithms.
The fixture uses a fictional internal URL and an unexecuted patch. Do not copy the backoff numbers as a latency SLO. Do not treat the free server option as a guarantee of isolation, retention, hardware, or a particular model catalog; those details belong in current product documentation at the time of use. Teams that need a legally reviewed payments or identity integration should not treat this article as a compliance guide.
Closing
Duplicate seats after a timeout are a protocol bug wearing a retry costume. A take-home that forces that distinction produces a clearer hiring signal than another greenfield CRUD app. Freeze the prompt, score the invariant, and treat unused requestId fields as evidence rather than leftover naming.
Top comments (0)