HTTP PUT vs POST in REST APIs: Idempotency and Resource Creation
Stop guessing when to use PUT vs POST in Spring Boot.
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
- ✓Basic understanding of REST APIs and HTTP methods
- ✓Familiarity with Spring Boot and Spring MVC annotations
- ✓Experience with Spring Data JPA or similar ORM
- Use POST to create resources when the client does not know the final URI; the server assigns one.
- Use PUT to create or replace a resource at a known URI; PUT is idempotent, POST is not.
- For partial updates, use PATCH, not PUT.
- In Spring Boot, use @PostMapping for POST and @PutMapping for PUT; ensure idempotency by handling duplicate requests gracefully.
- Never use PUT to append data to a collection; that's POST's job.
Think of a library. POST is like asking a librarian to add a new book to the shelf — you don't know where it will go. PUT is like handing the librarian a book and telling them exactly which slot to place it in. If you give them the same book twice for the same slot, the second time they'll just replace the old one with the new one (idempotent).
Every Spring Boot developer has faced this question: "Should I use PUT or POST for this endpoint?" And every team has that one debate that lasts 45 minutes and ends with someone saying, "Let's just use POST everywhere." I've been there. I've debugged production outages caused by choosing the wrong HTTP method. Let's settle this once and for all.
The core distinction is idempotency — a fancy word that means "making the same request multiple times has the same effect as making it once." PUT is idempotent; POST is not. That's not just academic. It directly impacts how you design your APIs, handle retries, and deal with network failures.
In this article, I'll show you exactly when to use each, with real Spring Boot code. We'll cover resource creation, idempotency guarantees, and the pitfalls that the official Spring docs gloss over. By the end, you'll never have to guess again.
The Fundamental Difference: Idempotency
Let's start with the HTTP specification, because that's where most developers stop reading. RFC 7231 defines POST and PUT clearly. But in practice, I see teams ignore these definitions and treat both as "create or update." That's a mistake.
POST is non-idempotent. It's designed for submitting data to a resource where the server decides the final URI. Think of posting a message to a forum — each post creates a new message with a new ID. If you send the same POST twice, you get two messages.
PUT is idempotent. It's designed for creating or replacing a resource at a specific URI. If you send the same PUT request twice, the second request has no additional effect — the resource is already there, identical.
In Spring Boot, you map these to @PostMapping and @PutMapping. But the real challenge is implementing idempotency correctly. Here's a common pattern: for creation endpoints where the client can generate an ID (like a UUID), use PUT. The client constructs the URL: PUT /api/orders/{orderId}. If the order doesn't exist, create it. If it does, replace it. This is idempotent by design.
But what if the client cannot generate an ID? Then you're stuck with POST. In that case, you need to implement idempotency keys. The client sends a unique key in the header (e.g., Idempotency-Key: uuid). The server stores that key with the result. If the same key comes again, return the stored result without processing. Spring doesn't have built-in support for this, but you can implement it with a filter and a cache.
Resource Creation: POST vs PUT in Spring Boot
The classic rule: POST creates a resource, PUT creates or replaces a resource at a known URI. But when should you use PUT for creation? Only when the client can determine the URI. This is common in systems where the client generates UUIDs, like in event sourcing or CQRS patterns.
In Spring Boot, implementing PUT creation is straightforward. You map the path variable to the resource ID. If the repository doesn't find the entity, you create it. If it does, you update it. The key is that the operation is idempotent — the same PUT request multiple times yields the same state.
For POST creation, the server generates the ID and returns it in the response body and the Location header. Spring Boot's ResponseEntity.created(URI) is your friend here. But remember: POST is not idempotent. If the client retries, you could end up with duplicate resources unless you implement idempotency keys.
Here's a practical tip: if your resource has a natural key (like an email or an ISBN), consider using PUT with that key as the identifier. For example, PUT /api/users/{email}. This makes the API idempotent and self-documenting. The client knows exactly what URI to use.
@PutMapping does not automatically handle creation vs update — you have to implement the logic. A common mistake is to assume that PUT only updates and return 404 if not found. That violates the HTTP spec. PUT should create if the resource doesn't exist.Partial Updates: Why PUT Is Not the Answer
I see this all the time: a developer wants to update only a few fields of a resource, so they use PUT with only those fields in the body. That's a bug waiting to happen. PUT is a full replacement. If you send only {"status": "SHIPPED"} to PUT /api/orders/123, the server might set all other fields to null.
In Spring Boot, if you use @PutMapping with a partial JSON, Jackson will deserialize missing fields as null. When you save the entity, those nulls overwrite existing values. The result? Data loss.
What you want is PATCH. PATCH is for partial modifications. But Spring's @PatchMapping doesn't automatically merge fields — you have to implement the merge logic yourself. Common approaches include using Map<String, Object> and applying changes manually, or using JSON Patch (RFC 6902).
If you absolutely must use PUT for updates, ensure the client sends the full representation. Validate that all required fields are present. This is one reason why many APIs prefer PUT for updates — it's simpler and idempotent. But it shifts the burden to the client.
My recommendation: Use PUT for full replacement, PATCH for partial updates. If you don't want to implement PATCH, design your endpoints to accept full representations and let the client handle the merge.
What the Official Docs Won't Tell You
The Spring Boot documentation is great for getting started, but it doesn't warn you about the real-world gotchas. Here are a few I've learned the hard way:
1. @PutMapping does not guarantee idempotency. Spring just maps the method to the HTTP verb. If your service method creates a new record every time without checking for existence, you've broken idempotency. You must implement the check yourself.
2. POST with @RequestBody and no validation can lead to duplicate resources. If you rely on the client to send a unique ID in the body, but you don't enforce uniqueness at the database level, a retry will create a duplicate. Always add unique constraints and handle DataIntegrityViolationException.
3. The Location header in POST responses is often forgotten. Many Spring Boot tutorials show returning the created entity with a 200 OK. That's wrong. The correct response is 201 Created with a Location header pointing to the new resource. Spring's ResponseEntity.created(URI) does this, but I still see code that returns ResponseEntity.ok(created).
4. PUT and POST are not interchangeable for file uploads. For file uploads, POST is the standard (RFC 1867). PUT can be used for uploading a file to a specific URI, but it's less common and not supported by all clients.
5. Spring Data REST uses POST for creation and PUT for replacement by default. But if you customize the repository, you might break idempotency. For example, if you override and always create a new record, PUT becomes non-idempotent.save()
repository.save(new Entity()) without checking for existence. The fix was to use findById first.When to Use POST Even for Idempotent Operations
There are valid reasons to use POST for idempotent operations. The most common is when the client cannot generate a meaningful URI. For example, if you're creating a resource in a collection where the ID is a sequential number assigned by the database, the client doesn't know the final URI until after creation.
In such cases, you can implement idempotency using a client-generated idempotency key. The client sends a unique key (e.g., a UUID) in the request header. The server stores the key with the result. If the same key arrives again, the server returns the stored result without processing.
This pattern is common in payment gateways like Stripe. Stripe's API uses POST for creating charges, but they require an idempotency key to prevent duplicate charges. This is a pragmatic compromise.
Another case is when the operation is not resource creation but an action (e.g., POST /api/orders/123/cancel). Even though the action might be idempotent (canceling an already canceled order does nothing), POST is used because the operation is not a CRUD create/update. The HTTP spec allows this — POST is the all-purpose method for actions that don't fit the other verbs.
In Spring Boot, you can implement idempotency keys using a filter that checks a cache (like Redis) before processing the request. If the key exists, return the cached response. If not, process and cache the result.
Common Mistakes with PUT and POST in Spring Boot
After years of code reviews and debugging, here are the mistakes I see most often:
Mistake 1: Using POST for all create operations, even when the client provides an ID. This breaks idempotency and forces you to implement idempotency keys unnecessarily. If the client has an ID, use PUT.
Mistake 2: Using PUT and forgetting to handle creation. Many developers implement PUT as an update-only endpoint, returning 404 if the resource doesn't exist. This is incorrect per HTTP spec. PUT should create the resource if it doesn't exist.
Mistake 3: Not returning the correct HTTP status codes. POST should return 201 Created with a Location header. PUT should return 200 OK (or 204 No Content). I often see POST returning 200 OK, which breaks REST conventions.
Mistake 4: Assuming idempotency is automatic. As I've stressed, Spring doesn't make your endpoint idempotent. You have to implement the logic.
Mistake 5: Using PUT for partial updates. This leads to data loss. Use PATCH or require full representations.
Mistake 6: Ignoring concurrency. Even if your PUT endpoint is idempotent, concurrent requests can cause race conditions. Use database transactions and optimistic locking via @Version to prevent lost updates.
The Double-Charge Disaster: When POST Was Used for Idempotent Payments
- If the client can generate a unique ID, use PUT with that ID in the URL for creation.
- Always enforce idempotency at the database level, not just in application code.
- Use idempotency keys for POST endpoints when you cannot use PUT.
- Timeout retries are dangerous with non-idempotent endpoints — implement idempotency or use at-most-once semantics.
- Test retry scenarios in integration tests with tools like WireMock.
grep 'POST' src/main/java/com/example/controller/*.javaCheck database unique constraints: SHOW INDEX FROM payments;| File | Command / Code | Purpose |
|---|---|---|
| OrderController.java | @RestController | The Fundamental Difference |
| UserController.java | @RestController | Resource Creation |
| OrderPatchController.java | @RestController | Partial Updates |
| IdempotentService.java | @Service | What the Official Docs Won't Tell You |
| IdempotencyFilter.java | @Component | When to Use POST Even for Idempotent Operations |
| VersionedEntity.java | @Entity | Common Mistakes with PUT and POST in Spring Boot |
Key takeaways
Interview Questions on This Topic
Explain the difference between PUT and POST in REST APIs. When would you use each?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't