How would you design a resource-creation call so that a client retrying after a network failure cannot create two records — by having the client choose the resource's URL and using PUT instead of POST to a collection?
answer
- Client mints the UUID before attempt one
- PUT /orders/{client-uuid} replaces POST /orders
- If-None-Match: * = create-only
- Unique constraint, never SELECT-then-INSERT
- Natural business key is the cheapest variant
basics
~20 sLet the client generate the identifier (a UUID) and PUT to /orders/{id}. The URL names one resource, so a replay writes the same record instead of creating a second. Guard the first write with If-None-Match: * so an existing resource is not silently overwritten.
solid answer
~50 sMove identifier generation to the client and turn the create into an idempotent write. - The client mints a UUID and calls `PUT /orders/9f3c...` with the full body. Because the URL identifies exactly one resource, a retry after a timeout targets the same record — at-least-once delivery collapses into one row. - Add `If-None-Match: *` so the write succeeds only if nothing exists there; that distinguishes "my own retry" from "someone else already owns this URL", which comes back as **412**. - Return **201** on the first write and **200/204** on a replay of the same body; if the replay carries a *different* body, decide explicitly — treat it as an update, or reject it. Trade-offs: the server no longer controls identity, so no sequential keys, and you depend on the client's UUID quality; and it only works when the client can name the resource. Where identity must be server-owned, the sibling idempotency-key mechanism covers the same need for `POST`.
code
http · 16 linesPUT /orders/9f3c1c0e-2f4d-4a20-9c88-1f9a0c1a77b3 HTTP/1.1
If-None-Match: *
Content-Type: application/json
{"sku":"A-100","qty":2}
HTTP/1.1 201 Created
ETag: "v1"
PUT /orders/9f3c1c0e-2f4d-4a20-9c88-1f9a0c1a77b3 HTTP/1.1
If-None-Match: *
Content-Type: application/json
{"sku":"A-100","qty":2}
HTTP/1.1 412 Precondition Failedgo deeper
Explain that the client makes a UUID and PUTs to that URL, so retrying writes the same record rather than making a second one.
Add If-None-Match: * for create-only semantics, the 201-vs-replay status handling, and enforcement via a unique constraint rather than check-then-insert.
Discuss where it does not fit (server-assigned identity, commands), natural-key alternatives, and the security posture of client-supplied keys.
Decide as policy which resources are client-named versus server-named, and how offline-capable clients and event producers depend on that choice.
## Why POST-to-collection is hazardous `POST /orders` means "process this per the collection's semantics", normally "create a new one". The server invents the identity, so the client has no name for what it may have created. After a timeout it has two bad options: retry and risk a second order, or give up and risk losing one. This is the double-submit hazard, and it appears equally from a user double-clicking Submit, an SDK auto-retry, a queue redelivery, or a proxy re-sending. ## The design move Give the client naming authority: 1. The client generates a UUIDv4 (or another collision-resistant identifier) **before** the first attempt. 2. It sends `PUT /orders/{id}` with the complete representation. 3. The server upserts at that key. Because `PUT` says "make the resource at this URL equal this state", replaying converges. Ten deliveries produce one order. The critical detail is *before the first attempt*. If the identifier were assigned by the server, the retry could not reuse it and the whole property collapses. ## Getting the status codes and preconditions right Use `If-None-Match: *` on a create-only write: it succeeds only when nothing exists at that URL. Then: - First write → **201 Created**, with the fresh `ETag`. - Replay of the same body → **200/204** (already in the desired state), or 201 again if you cannot distinguish; the client must treat both as success. - A different body at an existing URL → **412** if you enforce create-only, or an ordinary update if the endpoint is a genuine upsert. Pick one and document it, because "my retry" and "someone reusing my ID" otherwise look identical on the wire. Server-side, enforce it with a unique constraint on the primary key and map the constraint violation to the replay path rather than to a 500. Never implement it as "SELECT then INSERT": two concurrent retries can both pass the check. ## Where it fits and where it does not **Fits**: resources with natural or client-generatable identity — documents, orders, uploads, events from a producer that already has an event ID. It is also excellent for mobile and offline-first clients, which can create locally, keep working, and sync later against the same URL. **Does not fit**: when the identifier must be server-assigned (a human-facing sequential invoice number), when identity derives from server state, or when the operation is not a create at all — "capture this payment" is a command, not a resource you can name in advance. Those cases want the sibling idempotency-key approach or a reconciliation read. ## Security and hygiene A client-chosen key is attacker-influenced input. Use UUIDv4 or UUIDv7 rather than sequential or guessable values, validate the format strictly, and scope uniqueness per tenant if IDs are visible. Reject malformed or oversized inputs at the edge. And remember the URL is now stable and meaningful — it lands in logs and referrers, so do not encode anything sensitive in it. ## Related alternative: natural keys If the domain already has a unique business key — one enrollment per (user, course), one payment per invoice — a unique constraint on that key gives the same collapse for free, and the second attempt can return the existing resource. This is often the least machinery for the most safety, and it protects against duplicates from any source, not just retries.
- Why must the client generate the identifier before its first attempt rather than reusing one from a failed response?Because the failure that matters is exactly the one where no response came back. If identity is assigned server-side, a timed-out create leaves the client with no name to retry against, so the retry becomes a fresh create. Minting the ID up front is what makes the URL stable across every attempt and turns the create into an idempotent PUT.
- What is the drawback of letting clients choose primary keys?You lose server control over identity: no sequential or human-friendly numbering, key quality depends on the client's generator, and the key becomes attacker-influenced input that must be validated and scoped per tenant. Random UUIDs can also hurt index locality on some storage engines, which is why time-ordered variants like UUIDv7 are often preferred.
saying these in an interview costs you the question
- Implementing the create as SELECT-then-INSERT instead of relying on a unique constraint
- Returning 500 when the unique-key violation fires on a legitimate retry
- Accepting sequential or client-guessable identifiers without validation or tenant scoping
- Assuming a replayed PUT with a different body is still 'the same request'
- Claiming this technique works for commands like 'capture payment' that name no resource