🌐 Section 6 · Question #7

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/5 returns 204 No Content, and the second returns 404 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:

  1. Strict REST Semantics:
    • First call: 204 No Content (resource removed).
    • Subsequent retries: 404 Not Found (informing client that the targeted resource no longer exists).
  2. 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).

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.log or 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 returns 412 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:

  1. Query Parameter Lists (Synchronous Batch):
    DELETE /api/v1/users?ids=101,102,103 HTTP/1.1
    

    Retains the DELETE verb and idempotency: repeating the request leaves all specified IDs absent.

  2. Asynchronous Deletion Job Pattern (Large Datasets): For long-running bulk removals (e.g., purging 500,000 logs):
    • POST /api/v1/deletion-jobs $\to$ Returns 202 Accepted with Location: /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:

  1. The returned representation of the article itself is unchanged.
  2. 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 a GET request, 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:

  1. Browser Prefetching & Search Bots: Modern browsers, search crawlers (Google, Bing), and chat apps (Slack, WhatsApp unfurling links) speculatively execute GET requests on links found in pages or chats. If GET mutates state, visiting a link preview can wipe out user accounts.
  2. Intermediary Caching: Proxy servers, CDNs, and browser caches will cache the first GET response and serve it to subsequent users, skipping backend execution altogether.
  3. Automated Retries: Network proxies freely retry dropped GET requests, 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 DELETE request arrives, query the database within a transaction.
  • If the record is found, delete it and insert an OutboxEvent row 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 (or 204 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 DELETE with a JSON body {"ids": [1, 2, 3]}, though certain HTTP/1.1 proxies discard DELETE request bodies. For massive asynchronous purges, model a DeletionJob resource via POST /api/v1/deletion-jobs.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

  • GET is idempotent because it is a Safe (read-only) method under RFC 9110. Reading state causes zero domain mutations ($f(x) = x$).
  • DELETE is 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 Content on the first call and 404 Not Found on 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 DELETE requests 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 POST destroys protocol-level idempotency and prevents automated network retries).
  • ❌ “Soft delete breaks idempotency.” (Setting is_deleted = true repeatedly produces the exact same final state).