Why GET and DELETE are idempotent
Both GET and DELETE address opposite operational intents—retrieval and removal—yet both possess the fundamental HTTP protocol guarantee of Idempotency (RFC 9110).
🟢 Junior Level
Both GET and DELETE address opposite operational intents—retrieval and removal—yet both possess the fundamental HTTP protocol guarantee of Idempotency (RFC 9110).
An operation is idempotent if executing it multiple times with identical parameters leaves the server in the exact same resulting resource state as executing it once ($f(f(x)) = f(x)$).
1. Why GET Is Idempotent: Idempotency Through Safety
The GET method is semantically intended strictly for reading resource representations. It is defined as a Safe Method under RFC 9110, meaning it must produce zero mutations on domain resource state:
GET /api/v1/users/42 HTTP/1.1 --> 200 OK (User 42 representation)
GET /api/v1/users/42 HTTP/1.1 --> 200 OK (Identical User 42 representation)
Because reading data causes zero state changes ($f(x) = x$), executing GET $N$ times leaves the underlying database state completely unaltered.
2. Why DELETE Is Idempotent: Idempotency Through State Invariance
The DELETE method transitions the target resource to a deterministic final state: “the resource is absent from the system”.
// Request 1: Resource exists; server deletes it
DELETE /api/v1/users/42 HTTP/1.1 --> 204 No Content
// Request 2 (Automated network retry): Resource is already absent
DELETE /api/v1/users/42 HTTP/1.1 --> 404 Not Found (or 204 No Content)
Core Mental Model:
- After Request 1: User 42 does not exist in the system.
- After Request 2: User 42 still does not exist in the system.
- After Request 100: User 42 still does not exist in the system.
Because the persistent state of the server is completely identical following the 1st invocation and the 100th invocation, DELETE satisfies the definition of idempotency.
Summary Intuition:
• GET is idempotent because it mutates nothing.
• DELETE is idempotent because after the first call, there is nothing left to mutate.
🟡 Middle Level
The Status Code Independence Invariant (RFC 9110 Section 9.2.2)
A common interview question asks:
“If the first invocation of
DELETE /orders/5returns204 No Content, and the second returns404 Not Found, how can the method be idempotent when the HTTP status codes differed?”
Standard Clarification: RFC 9110 defines idempotency strictly by the side effects on server resource state, not by the identity of HTTP response status codes, bodies, or headers:
In production, architectures choose between two standard response strategies for repeated DELETE calls:
- Strict REST Semantics:
- First call:
204 No Content(resource removed). - Subsequent retries:
404 Not Found(informing client that the targeted resource no longer exists).
- First call:
- Silent Idempotency:
- Always return
204 No Content(the client’s goal was to ensure the resource does not exist; since it is absent, the goal is achieved).
- Always return
Both patterns are 100% compliant with HTTP idempotency.
Auxiliary Technical Side-Effects in GET
The RFC 9110 specification clarifies that Safe and Idempotent methods are permitted to trigger incidental technical side-effects, provided they do not alter the target resource representation:
- Writing access logs to
access.logor audit trails. - Incrementing Prometheus / Micrometer telemetry counters (
http_requests_total). - Warming internal caches in Redis or CDN edge nodes.
These operations are considered infrastructure mechanics, not business domain mutations, and do not violate REST safety contracts.
Soft Deletion (is_deleted = true)
In enterprise applications subject to compliance (GDPR, SOC2, financial auditing), physical deletion (DELETE FROM table) is often replaced with Soft Deletion:
UPDATE users SET is_deleted = TRUE, deleted_at = NOW() WHERE id = 42;
Even with soft deletion, the DELETE endpoint remains strictly idempotent: repeated calls simply keep is_deleted = true, resulting in an identical final database state.
🔴 Senior Level
Idempotent DELETE in Event-Driven Architecture (Kafka / Outbox Pattern)
In distributed microservices, deleting an aggregate typically triggers asynchronous domain events:
UserDeletedEvent(userId = 42).
If a client encounters a network timeout and automatically retries DELETE /api/v1/users/42, a naive implementation might publish a second UserDeletedEvent to Kafka, causing duplicate downstream notifications, email triggers, or billing adjustments.
Production Solution with the Transactional Outbox Pattern in Spring Boot 3
package com.example.service;
import com.example.entity.OutboxEvent;
import com.example.entity.User;
import com.example.repository.OutboxRepository;
import com.example.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
public class UserService {
private final UserRepository userRepository;
private final OutboxRepository outboxRepository;
public UserService(UserRepository userRepository, OutboxRepository outboxRepository) {
this.userRepository = userRepository;
this.outboxRepository = outboxRepository;
}
@Transactional
public boolean deleteUser(Long userId) {
Optional<User> userOpt = userRepository.findById(userId);
if (userOpt.isEmpty()) {
// Resource is already deleted!
// Skip publishing duplicate events to the Outbox table to guarantee event-stream idempotency
return false;
}
User user = userOpt.get();
userRepository.delete(user);
// Publish domain event exactly once inside the same ACID database transaction
OutboxEvent event = new OutboxEvent(
"USER_STREAM",
String.valueOf(userId),
"UserDeleted",
"{\"userId\":" + userId + "}"
);
outboxRepository.save(event);
return true;
}
}
package com.example.controller;
import com.example.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
boolean wasDeleted = userService.deleteUser(id);
if (wasDeleted) {
return ResponseEntity.noContent().build(); // 204 No Content on initial deletion
}
return ResponseEntity.notFound().build(); // 404 Not Found on subsequent retries
}
}
Conditional DELETE with Optimistic Locking (ETag / If-Match)
Under RFC 9110, DELETE requests can include conditional validation headers:
DELETE /api/v1/documents/100 HTTP/1.1
Host: api.example.com
If-Match: "v2-rev-etag"
- Guarantees the document is deleted only if the current version matches what the user previously inspected.
- If another concurrent user edited the document in the interim (advancing the ETag to
"v3-rev-etag"), the server aborts the deletion and returns412 Precondition Failed, protecting against accidental deletion of modified data.
Modeling Batch Deletions in REST
An anti-pattern in web APIs is using POST /api/v1/users/delete-batch to remove multiple entities. Using POST strips away protocol-level idempotency guarantees, preventing network proxies from safely retrying failed requests.
Idiomatic REST Approaches:
- Query Parameter Lists (Synchronous Batch):
DELETE /api/v1/users?ids=101,102,103 HTTP/1.1Retains the
DELETEverb and idempotency: repeating the request leaves all specified IDs absent. - Asynchronous Deletion Job Pattern (Large Datasets):
For long-running bulk removals (e.g., purging 500,000 logs):
POST /api/v1/deletion-jobs$\to$ Returns202 AcceptedwithLocation: /api/v1/deletion-jobs/89.- Client polls progress via idempotent
GET /api/v1/deletion-jobs/89.
4 Tricky Questions
1. If GET /articles/15 increments a view counter in the database, does it stop being Safe and idempotent?
Answer: Under RFC 9110 Section 9.2.1, auxiliary technical side effects (such as view counters, audit logs, or analytics tracking) do not disqualify a method from being Safe or Idempotent, provided:
- The returned representation of the article itself is unchanged.
- The user did not intend to execute a domain state modification.
However, architectural best practice warns against critical business side effects in
GET. If a financial charge or critical state mutation occurs during aGETrequest, search engine crawlers (Googlebot) and browser link prefetching tools will trigger unintended mutations.
2. Why is executing state mutations via GET (e.g., GET /users/1/delete) catastrophic in production?
Answer:
- Browser Prefetching & Search Bots: Modern browsers, search crawlers (Google, Bing), and chat apps (Slack, WhatsApp unfurling links) speculatively execute
GETrequests on links found in pages or chats. IfGETmutates state, visiting a link preview can wipe out user accounts. - Intermediary Caching: Proxy servers, CDNs, and browser caches will cache the first
GETresponse and serve it to subsequent users, skipping backend execution altogether. - Automated Retries: Network proxies freely retry dropped
GETrequests, compounding state mutations.
3. How do you prevent duplicate message emissions in event-driven systems when a DELETE endpoint is retried?
Answer: Implement existence checks coupled with the Transactional Outbox Pattern:
- When the
DELETErequest arrives, query the database within a transaction. - If the record is found, delete it and insert an
OutboxEventrow within the same ACID transaction. - If the record is not found (indicating a retry of an already completed deletion), do not insert an outbox event. Return
404 Not Found(or204 No Content) immediately. This ensures exactly-once event dispatch to Kafka/RabbitMQ.
4. How should batch deletion be modeled in REST to preserve idempotency?
Answer:
Batch deletions should use the DELETE method with target identifiers passed in query parameters:
DELETE /api/v1/resources?ids=1,2,3
- This preserves standard HTTP semantics, allowing network intermediaries to know the operation is idempotent and safe to retry.
- Alternatively, if payload bodies are preferred for large identifier lists, some architectures use
DELETEwith a JSON body{"ids": [1, 2, 3]}, though certain HTTP/1.1 proxies discardDELETErequest bodies. For massive asynchronous purges, model aDeletionJobresource viaPOST /api/v1/deletion-jobs.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
GETis idempotent because it is a Safe (read-only) method under RFC 9110. Reading state causes zero domain mutations ($f(x) = x$).DELETEis idempotent because it transitions the target resource to an invariant terminal state: “absent from the system”. Repeated executions find the entity already removed, leaving the server in the identical state.- Status Code Independence: Returning
204 No Contenton the first call and404 Not Foundon retries does not violate idempotency, because idempotency evaluates server resource state, not HTTP status codes.- Event-Driven Resilience: In microservices, use the Transactional Outbox pattern to ensure retried
DELETErequests do not emit duplicate Kafka domain events.
Red Flags (What NOT to Say)
- ❌ “DELETE is non-idempotent because the first call returns 204 and the second returns 404.” (Idempotency pertains to server state, not response status codes).
- ❌ “GET requests cannot write to database audit tables or metrics.” (RFC 9110 §9.2.2 explicitly permits logging, analytics, and telemetry side effects).
- ❌ “You should always use POST for batch deletion.” (Using
POSTdestroys protocol-level idempotency and prevents automated network retries). - ❌ “Soft delete breaks idempotency.” (Setting
is_deleted = truerepeatedly produces the exact same final state).
Related Topics
- What is idempotency — Fundamental idempotency theory
- Which HTTP methods are idempotent — Complete method taxonomy
- Is POST idempotent — POST processing mechanics
- What HTTP status codes do you know — Status codes (204 vs 404)
- What are the main HTTP methods used in REST — Method semantics