Is POST idempotent
An operation is non-idempotent if executing it multiple times with identical parameters produces cumulative side effects or alters the server's resource state with each invocation.
🟢 Junior Level
No, by official HTTP specification (RFC 9110), the POST method is strictly NON-IDEMPOTENT and NON-SAFE.
An operation is non-idempotent if executing it multiple times with identical parameters produces cumulative side effects or alters the server’s resource state with each invocation.
Concrete Example: E-Commerce Checkout
Consider placing an order in an online store:
POST /api/v1/orders HTTP/1.1
Host: shop.example.com
Content-Type: application/json
{
"item": "Laptop",
"quantity": 1,
"price": 1200.00
}
- Execution 1: The server creates Order
#101and charges the credit card$1,200. - Execution 2 (Network Retry): The server creates Order
#102and charges the card a second time ($2,400total). - Execution 3 (Accidental Refresh): The server creates Order
#103and charges the card a third time ($3,600total).
Because each repeated invocation produces an independent, distinct resource and triggers duplicate business side effects, POST is non-idempotent.
Browser Protection: “Confirm Form Resubmission”
Web browsers recognize that POST is non-idempotent. If a user attempts to refresh (F5) or navigate back to a page rendered directly by a POST request, the browser presents a modal dialog:
“The page that you’re looking for used information that you entered. Returning to that page might cause any action you took to be repeated. Do you want to continue?”
🟡 Middle Level
The Semantic Role of POST (RFC 9110 Section 9.3.3)
Under the HTTP standard, POST is designated as the universal catch-all processing verb for:
- Subordinate Resource Creation: The client targets a collection URI (
/users), and the server generates and assigns a new identifier (/users/105). - Executing Business Commands: Invoking arbitrary domain processes (
/api/v1/tax-calculations,/api/v1/notifications/dispatch). - Appending Data: Adding sequential entries to an existing message log, audit trail, or event stream.
Because the server retains full control over identifier generation and internal business dispatch, successive requests inherently create new state.
The Post/Redirect/Get (PRG) Pattern
In web applications, the PRG pattern prevents duplicate POST execution caused by browser page refreshes:
User (Browser) Application Server (Spring Boot)
│ │
├───── 1. POST /checkout ──────────────────────────────────>│ (Charges card & saves order #42)
│ │
│<──── 2. HTTP/1.1 303 See Other ───────────────────────────┤
│ Location: /orders/42 │
│ │
├───── 3. GET /orders/42 ──────────────────────────────────>│ (Safe, idempotent read)
│ │
│<──── 4. HTTP/1.1 200 OK (Order Summary HTML/JSON) ────────┤
If the user presses F5, the browser re-executes the safe, idempotent GET /orders/42 request, completely preventing duplicate payments or duplicate orders.
The Pragmatic Exception: POST for Complex Queries (POST as Read-Only)
While data retrieval strictly belongs to GET, production enterprise architectures routinely use POST for search:
- Complex Search Filters: When search criteria include dozens of nested filters, multi-field faceting, or coordinate arrays, serializing them into a URL query string exceeds web server and proxy URI length limits (typically $2\text{ KB} - 8\text{ KB}$).
- GraphQL: All GraphQL operations (including
queryreads) are dispatched viaPOSTpayloads.
[!NOTE] Even though
POST /api/v1/searchis implemented internally as read-only, HTTP clients, intermediate proxies, and CDNs treat it as non-idempotent. They will not cache the response by default and will not auto-retry it on network dropouts. (The IETF is developing the standard HTTPQUERYmethod to officially support safe reads with request bodies).
🔴 Senior Level
Enterprise Pattern: Making POST Idempotent via Idempotency-Key
In financial microservices (Stripe, Adyen, PayPal), network packet loss must never result in duplicate billing. To make a business operation executed via POST fully idempotent, systems combine an Idempotency-Key, a Redis Distributed Lock, and Database Unique Constraints.
Distributed Idempotency Workflow
1. Client generates UUID v4 and sends: `Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d`
2. Server queries Redis:
├── Status = "COMPLETED": Replay cached HTTP response (skip business execution).
├── Status = "PROCESSING": Return `409 Conflict` (prevents concurrent race conditions).
└── Key not found: Acquire distributed lock via `SET lock:key "PROCESSING" NX EX 120`.
3. Server executes business transaction in database.
4. Server saves result payload in Redis with status "COMPLETED" (TTL 24 hours).
5. Server releases the processing lock.
Production Implementation in Spring Boot 3
package com.example.service;
import com.example.dto.PaymentRequest;
import com.example.dto.PaymentResponse;
import com.example.entity.Payment;
import com.example.exception.ConcurrentPaymentException;
import com.example.repository.PaymentRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
@Service
public class PaymentProcessingService {
private static final Duration LOCK_TTL = Duration.ofMinutes(1);
private static final Duration RESULT_TTL = Duration.ofHours(24);
private final StringRedisTemplate redisTemplate;
private final PaymentRepository paymentRepository;
private final ObjectMapper objectMapper;
public PaymentProcessingService(StringRedisTemplate redisTemplate,
PaymentRepository paymentRepository,
ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate;
this.paymentRepository = paymentRepository;
this.objectMapper = objectMapper;
}
public PaymentResponse processPayment(String idempotencyKey, PaymentRequest request) {
String lockKey = "idemp:lock:" + idempotencyKey;
String dataKey = "idemp:result:" + idempotencyKey;
// 1. Check if previously completed result is cached
String cachedJson = redisTemplate.opsForValue().get(dataKey);
if (cachedJson != null) {
return deserializeResponse(cachedJson);
}
// 2. Atomically acquire distributed lock
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "PROCESSING", LOCK_TTL);
if (Boolean.FALSE.equals(acquired)) {
throw new ConcurrentPaymentException("Payment with this idempotency key is already processing");
}
try {
// 3. Execute business transaction
PaymentResponse response = executePaymentTransaction(idempotencyKey, request);
// 4. Cache successful result for safe automated retries
String responseJson = objectMapper.writeValueAsString(response);
redisTemplate.opsForValue().set(dataKey, responseJson, RESULT_TTL);
return response;
} catch (Exception ex) {
throw new RuntimeException("Payment processing failed", ex);
} finally {
// 5. Release processing lock
redisTemplate.delete(lockKey);
}
}
@Transactional
protected PaymentResponse executePaymentTransaction(String idempotencyKey, PaymentRequest req) {
try {
Payment payment = new Payment();
payment.setIdempotencyKey(idempotencyKey);
payment.setAccountId(req.accountId());
payment.setAmount(req.amount());
payment.setStatus("CONFIRMED");
Payment saved = paymentRepository.save(payment);
return new PaymentResponse(saved.getId(), saved.getStatus(), saved.getAmount());
} catch (DataIntegrityViolationException ex) {
// Caught database unique constraint violation if a cluster race occurred
Payment existing = paymentRepository.findByIdempotencyKey(idempotencyKey).orElseThrow();
return new PaymentResponse(existing.getId(), existing.getStatus(), existing.getAmount());
}
}
private PaymentResponse deserializeResponse(String json) {
try {
return objectMapper.readValue(json, PaymentResponse.class);
} catch (Exception e) {
throw new IllegalStateException("Failed to parse cached payment response", e);
}
}
}
Request Payload Integrity (Tampering Detection)
If a client transmits a request with an existing Idempotency-Key but alters the payload (e.g., amount: 500 instead of the original amount: 100), the server must not return the cached result.
Standard Protocol Solution:
- Upon initial request, calculate the SHA-256 cryptographic digest of the raw request payload body and store it in Redis alongside the idempotency record.
- When a subsequent request arrives with the same key, recalculate the incoming payload’s SHA-256 hash.
- If the digests differ, reject the request immediately with
422 Unprocessable Contentor400 Bad Request:"Idempotency-Key was previously used with a different request payload".
4 Tricky Questions
1. Can POST ever be used for an operation that does not mutate data, and does that violate REST?
Answer:
Yes, it is routinely used for read-only operations such as complex search filtering (POST /api/v1/products/search) or GraphQL queries.
- This does not strictly violate REST if pragmatic constraints (such as URL length limits in browsers and reverse proxies) prevent sending complex JSON filter graphs in a
GETquery string. - However, developers must recognize that at the protocol level, clients and intermediate proxies (CDNs) will still treat
POSTas non-idempotent: responses are not cached by default, and network layers will not automatically retry dropped requests.
2. What happens if two identical POST requests with the same Idempotency-Key arrive at two separate server nodes concurrently?
Answer: A race condition occurs. To prevent duplicate execution:
- Both nodes attempt an atomic distributed lock operation in Redis (
SET lock:key "PROCESSING" NX EX 120). - Only one node succeeds in acquiring the lock. That node proceeds to execute the business logic.
- The losing node fails to acquire the lock and immediately returns
409 Conflict(or waits for the lock to clear and serves the cached response). - As a secondary barrier, the database table enforces a
UNIQUE(idempotency_key)constraint, ensuring that even if Redis fails, the relational database transaction rolls back on duplicate insert.
3. What is the fundamental difference between creating a resource via PUT versus POST regarding idempotency?
Answer:
- Creation via
PUT /users/42: The client dictates the exact URI and identifier of the resource. If the client repeats the request 5 times, all 5 requests overwrite the identical URI/users/42with the same representation. Hence,PUTcreation is strictly idempotent. - Creation via
POST /users: The client targets a collection URI. The server assigns the resource identifier upon creation (/users/42,/users/43,/users/44). Repeating the request creates a new subordinate resource with a new identifier each time. Hence,POSTcreation is non-idempotent.
4. What problem does the Post/Redirect/Get (PRG) pattern solve, and how does it leverage HTTP status codes?
Answer:
The PRG pattern prevents duplicate POST submissions when a user refreshes their browser page (F5).
- Instead of returning a
200 OKHTML response directly to aPOSTrequest, the server responds with303 See Other(or302 Found) containing aLocation: /resource/123header. - The browser intercepts the redirect and automatically executes a safe, idempotent
GET /resource/123. - The page displayed in the browser is now the result of a
GETrequest. If the user refreshes, the browser safely re-fetches the resource without re-submitting the payment or order form.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
By official HTTP specification (RFC 9110), the
POSTmethod is strictly non-idempotent and unsafe.Semantically,
POSTis designed for subordinate resource creation or processing commands, meaning executing it $N$ times produces $N$ independent resources or side effects. Because of this, browsers prompt “Confirm Form Resubmission”, and network proxies forbid automatic retries.In production distributed systems,
POSTis made idempotent at the application layer using theIdempotency-Keypattern, Redis distributed locks (SETNX), request body checksums, and databaseUNIQUEconstraints.
Red Flags (What NOT to Say)
- ❌ “POST is idempotent if we only update an existing database record.” (Regardless of application code,
POSTis non-idempotent by protocol specification). - ❌ “Service meshes and HTTP clients can safely auto-retry failed POST requests on timeout.” (Auto-retrying
POSTleads to duplicate credit card charges and duplicate database rows). - ❌ “Idempotency keys should be stored in a local Java HashMap on the server.” (Fails in multi-node clusters; idempotency state must be shared via Redis or a database).
- ❌ “The PRG pattern is an optimization to reduce database queries.” (PRG is a web architectural pattern designed to prevent duplicate form submissions on browser page refresh).
Related Topics
- What is idempotency — Mathematical and architectural foundations
- Which HTTP methods are idempotent — Complete method taxonomy
- Why GET and DELETE are idempotent — State invariance analysis
- What is the difference between PUT and PATCH — Complete vs. partial mutation
- Should you use verbs in URL — Resource-oriented URI design