When is HTTP status 409 Conflict the right answer in an API contract, and how does it differ from 422 and from 412 Precondition Failed?
answer
- 409 = valid request, clashes with current state
- Would it be valid on an empty system? yes → 409
- 412 = client's If-Match validator failed
- 428 = precondition required but missing
- 409 body: machine code + current state; not blindly retryable
basics
~20 s409 means the request is valid in itself but clashes with the resource's current state — a duplicate unique key, an illegal state transition, a concurrent edit. 422 means the request is invalid on its own terms; 412 means an explicit precondition header the client sent did not hold.
solid answer
~50 sThink about *where the problem lives*. - **422** — in the request body alone. `endDate` before `startDate` is wrong no matter what the server holds. - **409** — in the relationship between the request and current state. Creating a user with an email that already exists; cancelling an order that already shipped; two clients editing the same record without preconditions. - **412** — the client explicitly stated a condition via `If-Match`/`If-Unmodified-Since` and it did not hold. 412 is the *cooperative* version of the same collision: the client asked to be told. So: if the client sent a validator and it is stale → `412`, and the client knows to re-read and retry. If there is no validator and you detect a clash → `409`, with a body naming the conflicting field or the current state so the client can decide. Good 409 bodies include the current state (`"currentStatus":"SHIPPED"`) or the conflicting field. Never make 409 blindly retryable — a retry that cannot succeed is just load.
go deeper
Know that 409 means the request clashes with the current state of the resource, such as a duplicate email.
Distinguish it from 422 with the empty-system test and give concrete examples like illegal state transitions.
Add 412/428 and conditional requests, mapping database constraint violations, actionable 409 bodies, and why 409 is not auto-retryable.
Decide platform-wide how conflicts are represented and whether optimistic concurrency uses ETags and 412 uniformly rather than ad-hoc 409s per service.
## The definition 409 Conflict: the request could not be completed because of a conflict with the current state of the target resource, and the client could plausibly resolve it and resubmit. That last clause is the useful part — 409 implies *the situation may be fixable by the client*, which is what separates it from a flat 403 or a 400. ## The canonical 409 cases **1. Uniqueness violation on create.** `POST /users` with an email already registered. The body is well-formed and every field is valid; the clash is with existing data. 409 with `{"code":"EMAIL_TAKEN","field":"email"}` is the accurate answer. (Note the security wrinkle: on a public signup form, telling the caller that an email is taken is an account-enumeration oracle. Where that matters, answer 202/200 generically and send an email instead — the correct status is the one that does not leak.) **2. Illegal state transition.** `POST /orders/42/cancel` on an order that already shipped. Nothing in the request is malformed; the resource is simply not in a state where the operation makes sense. Return 409 and include the current state so the client can render "this order already shipped" instead of a generic error. **3. Concurrent modification without preconditions.** Two writers, last-write-wins detected server-side via a version column. If the client did not send a validator, 409 is how you tell it something moved under its feet. **4. Structural conflicts.** Deleting a resource other rows still reference; adding a member who is already in a group; a lock held by someone else. ## 409 versus 422 The test: *would this request be valid against an empty system?* If yes, and it only fails because of what already exists, it is 409. If it would fail regardless of stored state, it is 422 (or 400). "Quantity must be positive" is 422 forever; "SKU already exists" is 409 only because of the current inventory. ## 409 versus 412 Both are collisions, but 412 requires the client to have opted in: ``` PUT /orders/42 If-Match: "v7" HTTP/1.1 412 Precondition Failed ``` The client said "only if it is still version 7", the server checked, and it was not. The client knows exactly what to do: GET the current version, re-apply, retry. That is the well-behaved path, and if your API *requires* preconditions you can answer `428 Precondition Required` when the header is missing, nudging clients into it. 409 is what you return when there is no validator to check — either the API does not use conditional requests, or the conflict is domain-level (state machine, uniqueness) rather than version-level. Some APIs also use 409 for optimistic-lock failures on a body-carried version field; that is fine, but 412 with an ETag is the more standard mechanism and interoperates with generic HTTP tooling. ## Making 409 actionable A bare 409 is nearly useless to a client. Include: - a **stable machine code** (`ORDER_ALREADY_SHIPPED`, `DUPLICATE_EMAIL`), - the **current state** or the conflicting field/value, - where relevant, a **link to the existing resource** ("a user with this email is at /users/17") — but only when disclosing that is safe, - an explicit statement of whether retrying can help. That last point drives client behaviour: 409 is generally **not** automatically retryable. A client library that blindly retries 4xx on conflict just burns quota. Retry only after the client has re-read state and rebuilt the request — which is a user- or application-level decision, not a transport-level one. ## Implementation notes Race conditions make 409 unavoidable even with pre-checks: "check if the email exists, then insert" has a window, so the database unique constraint will still fire. Catch the constraint violation and map it to 409 rather than letting it surface as a 500. That mapping — unique violation → 409, foreign-key violation → 409 or 422 depending on whether the referenced thing is client-supplied — is worth doing once in a shared exception handler. Also keep 409 out of your error-rate alerting thresholds for user-driven flows: a steady trickle of "already cancelled" conflicts is normal product behaviour, whereas a sudden spike may indicate a client retry bug or a broken idempotency layer.
- A signup endpoint gets an email address that is already registered. Is 409 the right answer?Technically yes — it is a uniqueness conflict with current state. But on a public signup form it confirms that the address has an account, which is an enumeration oracle useful for credential stuffing and phishing. The safer contract returns a generic success-shaped response and sends an email that either completes signup or tells the owner someone tried, so the API leaks nothing to the caller.
- Should clients automatically retry a 409?No, not blindly. A conflict means the current state contradicted the request, so an identical retry usually fails identically and just adds load. The correct pattern is to re-read the resource, decide whether the intent still applies, rebuild the request, and resubmit — often with a user in the loop. Automatic retry is appropriate for 429 and some 503s, not 409.
saying these in an interview costs you the question
- Using 409 for plain validation failures that have nothing to do with stored state
- Letting a database unique-constraint violation escape as a 500 instead of mapping it to 409
- Returning a bare 409 with no code, no field and no current state
- Retrying 409 automatically in a client library
- Using 409 for a stale conditional request when the client sent If-Match and 412 is defined for exactly that