All articles
HTTPAPI DesignSystem Design12 min read

PUT vs PATCH vs POST: Choosing the Right Write Method

The three write methods compared on idempotency, partial updates, concurrency control and retry safety — with JSON Merge Patch, JSON Patch, ETags and a decision tree you can defend in an interview.

The actual difference

All three write. The distinction is not “create vs update” — that shorthand is wrong and causes most of the confusion. The three real questions are:

  1. Who chooses the URI? POST lets the server choose. PUT and PATCH target a URI the client already knows.
  2. Is the payload complete? PUT sends the whole resource. PATCH sends a diff.
  3. What happens on a retry? PUT and (well-designed) PATCH converge to the same state. POST creates a second thing.
One sentence version
POST: “here is some data, do the right thing with it and tell me where it landed.” PUT: “make the thing at this URI look exactly like this.” PATCH: “change these specific fields of the thing at this URI.”

Side-by-side comparison

POSTPUTPATCH
TargetA collectionA specific resourceA specific resource
URI chosen byServerClientClient (must exist)
PayloadNew resource or action inputComplete representationPartial diff
IdempotentNoYesOnly if absolute
Creates if absentYes (that is the point)Yes (upsert)No → 404
Omitted fieldsn/aCleared / reset to defaultLeft untouched
Typical success201 Created + Location200 or 204200 with new state
CacheableNo (practically)NoNo
Safe to blind-retryNo — needs an idempotency keyYesYes if absolute

When POST is right

  • Creating into a collection where the server generates the id.
  • Anything that is not a resource mutation: sending an email, running a job, cancelling an order.
  • Operations that are inherently non-idempotent, like “append a comment”.
  • Large or sensitive query payloads that must not sit in a URL or an access log.
POST /v1/articles
Content-Type: application/json
Idempotency-Key: 4b1e-90cc

{"title":"HTTP methods","body":"..."}

HTTP/1.1 201 Created
Location: /v1/articles/771
Create with server-assigned identity.

When PUT is right

Use PUT when the client owns the full state and the identity. Configuration documents, user settings blobs, file uploads at a known path, and “sync this whole object from my local copy” flows are all natural PUTs.

PUT /v1/users/42/preferences
{"theme":"light","emailDigest":true,"timezone":"Asia/Kolkata"}

HTTP/1.1 200 OK
PUT is an upsert at a client-known URI.
The PUT trap
PUT means replace. If your client fetched the object, changed one field, and PUT it back, any field added by the server since the fetch is now erased. This is a lost-update bug that only appears after a deploy adds a new column. Guard it with If-Match, or use PATCH.

When PATCH is right

Use PATCH for field-level edits from a UI — toggling a flag, renaming something, changing a status. It is the smallest possible payload and it does not touch fields the client has never heard of, which makes it forward-compatible as the schema grows.

PATCH /v1/users/42
Content-Type: application/merge-patch+json
If-Match: "v19"

{"timezone":"Europe/London"}

Two subtleties. PATCH on a non-existent resource should return 404, not create — creation via PATCH is a PUT with extra steps. And PATCH is idempotent only when the diff is absolute; a payload like {"op":"increment","field":"views"} breaks on every retry.

Merge Patch vs JSON Patch

JSON Merge Patch (RFC 7396)JSON Patch (RFC 6902)
Content-Typeapplication/merge-patch+jsonapplication/json-patch+json
ShapeLooks like the resourceArray of operations
Delete a fieldSet it to null{ "op": "remove" }
ArraysReplaced wholesaleIndex-level add/remove/move
Set a field to nullImpossible — null means deleteSupported
ReadabilityHighLow
Best forMost CRUD APIsDocuments, collaborative editing, precise array edits
// Merge Patch
{ "name": "Ada", "nickname": null }

// JSON Patch
[
  { "op": "replace", "path": "/name",     "value": "Ada" },
  { "op": "remove",  "path": "/nickname" },
  { "op": "add",     "path": "/tags/-",   "value": "admin" }
]
The same change in both formats.

Pick Merge Patch unless you have a concrete need for array surgery or the ability to distinguish “set to null” from “remove”. JSON Patch is more powerful and materially harder to validate, authorise per field, and audit.

Concurrency: ETags and If-Match

Two users editing the same record is not an edge case, it is Tuesday. Optimistic concurrency with ETags turns a silent lost update into an explicit 412.

GET /v1/users/42            → 200, ETag: "v19"

PATCH /v1/users/42
If-Match: "v19"             → 200, ETag: "v20"

PATCH /v1/users/42
If-Match: "v19"             → 412 Precondition Failed
Read the version, send it back, let the server reject stale writes.
  • The ETag can be a row version, an updated_at hash, or a content hash.
  • Use If-None-Match: * on PUT to mean “create only if it does not exist”.
  • On 412 the client refetches and either retries or shows a conflict UI. Never auto-merge silently.

Retries and idempotency keys

A timeout is not a failure — it is an unknown. The request may have succeeded and only the response was lost. PUT and absolute PATCH are safe to resend because the end state is the same. POST is not, so you have to add the guarantee yourself.

// pseudo
const key = req.header("Idempotency-Key");
const seen = await store.get(key);            // scoped per endpoint + user
if (seen) return respond(seen.status, seen.body);

const result = await createOrder(req.body);
await store.set(key, result, { ttl: "24h" }); // store BEFORE responding
return respond(201, result);
Server-side dedupe keyed on the client's key.
  • Store the key and the response inside the same transaction as the write, or you can still double-create.
  • Reject a reused key with a different body — that is a client bug, return 422.
  • Expire keys; 24 hours comfortably covers any sane retry policy.

Decision tree

Does the client know the URI?i.e. does the resource already exist there?noPOST /collectionserver assigns id → 201 + LocationyesIs the payload the complete state?every field presentyesPUT /resource/ididempotent full replacenoPATCH /resource/idpartial, send If-MatchException: PUT is also correct for client-chosen ids — PUT /files/report-2026.pdf creates or replaces.
Three questions, one method. Most disagreements in code review resolve here.

Interview framing

“Creates are POST to the collection with an idempotency key, returning 201 and a Location header. Full-document writes — settings, uploads at a known path — are PUT, which is idempotent so our retry layer can resend them freely. UI field edits are PATCH with JSON Merge Patch and an If-Match ETag, so a stale editor gets a 412 instead of silently clobbering another user's change. We never PUT a partially-populated body, because omitted fields are semantically deletions.”

Keep reading

Suggested next articles based on this one.

Design it, don't just read it.

Practise LLD and system design problems with structured rubrics and AI feedback.

Start practising free