Prompt and setting
You own POST /exports for a large export. The request validates input and starts work, but the export may take minutes. The interviewer asks whether to return 200, 201, or 202, and how a client learns the final result.
Assume the server can persist an export record and enqueue work. The client may time out and repeat the request. The answer must define the contract, not just name a status code.
What the interviewer tests
- Whether you distinguish “resource created” from “request accepted for later processing”.
- Whether you model a durable status resource, terminal states, and error details.
- Whether a retry can create two exports or lose the original response.
- Whether the queue and database update are reliable without pretending to have a distributed transaction.
Clarifying questions before answering
- Does the POST create a durable export resource immediately? If yes,
201 Createdmay describe that resource; if no,202can acknowledge an accepted job. - Can the same logical request be retried? If yes, require an idempotency key or a caller-provided operation ID.
- Does the client need polling, webhooks, or both? This changes the status representation and notification contract.
- What are the retention and authorization rules for exported files? A completed job is not permission to expose its result to every caller.
30-second answer framework
“I return 202 Accepted only when processing is deferred and the final result is not ready. I create a durable job record first, then return its status URI and an operation identifier. The client polls that URI with backoff or receives an authenticated callback. An idempotency key maps retries to the same job and response. The job moves through explicit states such as queued, running, succeeded, and failed; the worker and outbox are retryable, and the status endpoint remains the source of truth.”
Step-by-step deep dive
1. Choose the status from the resource lifecycle
200 OK means the request completed with a representation. 201 Created means a resource was created and should be identifiable. 202 Accepted means the request was accepted, while processing may not have started or finished; it does not promise eventual success.
If the export record is created synchronously and is the resource the caller will manage, I can return 201 with that resource. If the API only acknowledges work and the result is pending, 202 plus a monitor URI is clearer. The choice follows the observable lifecycle, not the fact that a queue happens to exist.
2. Make the response actionable
Return an operation ID, a status URL, and a representation with state, timestamps, and a safe retry hint. A minimal response can look like this:
HTTP/1.1 202 Accepted
Location: /exports/exp_123
Retry-After: 5
Content-Type: application/json
{"id":"exp_123","state":"queued","status_url":"/exports/exp_123"}The status resource should use authorization on every read. queued and running are non-terminal. succeeded includes a short-lived download reference; failed includes a stable error code and a remediation hint without leaking stack traces. A client must tolerate the resource disappearing after its retention window.
3. Make retries converge
Require Idempotency-Key for operations that create work. Persist a hash of the relevant request, the resulting job ID, and the response status. A repeated key with the same request returns the original result; the same key with a different request is a client error. Do not use a time window alone as the identity rule, because a late retry can arrive after the window.
The API can still accept two distinct keys for two exports. Idempotency prevents duplicate work for one logical operation; it does not make workers exactly once.
4. Close the database-to-queue gap
Write the export row and an outbox event in one database transaction. A relay publishes pending outbox rows and marks them delivered after the broker acknowledges them. A crash can publish the same event again, so the consumer uses the export ID as an idempotency key. This preserves the invariant that a committed job is eventually discoverable without claiming that a database and broker commit atomically.
The worker updates state with conditional transitions, for example queued -> running -> succeeded|failed. A stale retry cannot move succeeded back to running. Metrics should expose queue age, running age, terminal failure rate, and outbox lag.
5. Define polling, callbacks, and cancellation
The status endpoint supports ETag or a version so polling can use conditional requests. Clients apply exponential backoff with a server hint and stop polling after a terminal state. Webhooks are an optimization, not the only way to learn the result: delivery can fail, so the client must reconcile by reading the status resource.
Cancellation is a separate command, such as POST /exports/exp_123/cancel. It is accepted only for cancellable states and is itself idempotent. A job that already reached succeeded cannot be rolled back by a late cancel.
High-quality sample answer
I would first ask whether the export record is a resource created by this call. If it is, I may return 201 and the record. For a deferred operation whose result is not ready, I return 202 with an operation ID and an authenticated status URL. I require an idempotency key, store the request fingerprint and job ID, and return the same representation for a retry.
The transaction writes the export row and an outbox event together. A relay and an idempotent consumer handle delivery at least once. The status state machine is monotonic: queued, running, then succeeded or failed. The client polls with conditional requests and backoff; a webhook is only an accelerator. I publish queue age, outbox lag, and terminal errors, and I define retention, authorization, download expiry, and cancellation separately. 202 acknowledges acceptance, not success.
Common mistakes
- Error: Treating
202as proof that work will succeed → Why it fails: the RFC semantics allow processing to fail or never begin → Fix: expose terminal failure and retention behavior. - Error: Returning only
202with no monitor URL → Why it fails: clients cannot discover state without guessing → Fix: return an authenticated status resource and operation ID. - Error: Relying on a queue publish after the database commit → Why it fails: a crash creates a job that no worker can see → Fix: use a transactional outbox and replayable relay.
- Error: Assuming a queue gives exactly-once execution → Why it fails: retries and crashes can duplicate delivery → Fix: make consumers idempotent and transitions conditional.
- Error: Letting every idempotency-key reuse return success → Why it fails: a key can hide a changed request → Fix: compare a request fingerprint and reject mismatches.
Follow-up questions and responses
Should this endpoint return 201 instead?
Return 201 when the synchronous call creates a durable export resource and can identify it with Location. Return 202 when the meaningful result is deferred and the response only acknowledges acceptance. Some APIs can use 201 for the job resource while still exposing a pending state; document which resource the status describes.
What if the client never polls?
Keep the job state durable for the documented retention period, send an optional authenticated webhook, and allow a later GET by operation ID. A callback failure must not delete the only status path. Expired download URLs and authorization checks still apply when the client returns days later.
Can the worker update the job twice?
Yes, delivery is normally at least once. Use a unique export ID, conditional state transitions, and idempotent output writes. A duplicate succeeded event should be harmless; a transition from a terminal state back to running should be rejected and logged.