Чому GET та DELETE ідемпотентні
Методи GET та DELETE вирішують протилежні завдання (читання та видалення), але обидва мають фундаментальну властивість протоколу HTTP — ідемпотентність.
🟢 Junior Level
Методи GET та DELETE вирішують протилежні завдання (читання та видалення), але обидва мають фундаментальну властивість протоколу HTTP — ідемпотентність.
Це означає, що повторення одного й того самого запиту залишає сервер у тому самому цільовому стані, що й одноразовий запуск.
1. Чому GET ідемпотентний
Метод GET призначений виключно для отримання (читання) представлення ресурсу. Він не змінює стан даних у базі даних:
GET /api/v1/users/42 HTTP/1.1 -> 200 OK (дані користувача)
GET /api/v1/users/42 HTTP/1.1 -> 200 OK (ті самі дані)
Оскільки GET є безпечним (Safe) методом, його повторний виклик $N$ разів залишає дані недоторканими ($f(x) = x$).
2. Чому DELETE ідемпотентний
Метод DELETE переводить цільовий ресурс у кінцевий стан: «ресурс у системі відсутній».
// 1-й запит: ресурс був у БД, ми його видалили
DELETE /api/v1/users/42 HTTP/1.1 -> 204 No Content
// 2-й запит (повтор через збій мережі): ресурс уже відсутній
DELETE /api/v1/users/42 HTTP/1.1 -> 404 Not Found (або 204 No Content)
Ключовий висновок: І після першого виклику, і після десятого виклику ресурс users/42 у системі відсутній. Підсумковий стан сервера ідентичний, що суворо задовольняє визначення ідемпотентності.
🟡 Middle Level
Різні коди відповіді при DELETE та ідемпотентність
Одне з найчастіших питань на технічних інтерв’ю:
«Якщо перший виклик
DELETEповертає200 OKабо204 No Content, а другий повертає404 Not Found, чому метод вважається ідемпотентним? Адже коди відповіді різні!»
Пояснення за стандартом RFC 9110 (розділ 9.2.2): Ідемпотентність визначається виключно ефектом, який чиниться на стан сервера (side effects on the server state), а не статус-кодом HTTP-відповіді.
Існує дві поширені стратегії відповіді на повторний DELETE:
- Сувора REST-семантика: Перший виклик повертає
204 No Content, повторні виклики —404 Not Found(клієнту повідомляється, що ресурсу вже немає). - Тиха ідемпотентність (Silent Idempotency): Завжди повертати
204 No Content, навіть якщо ресурсу вже не було (клієнт просив, щоб ресурсу не було — його немає, мета досягнута).
Обидві стратегії є повністю ідемпотентними.
Технічні побічні ефекти при GET (Access Logs, Metrics, Cache)
У специфікації HTTP термін «Safe method» означає відсутність передбачуваних клієнтом мутацій цільового ресурсу. Проте GET запит має повне право:
- Додавати записи в журнал доступу (
access.log) та аудит. - Інкрементувати лічильники в Prometheus / StatsD.
- Прогрівати кеш у Redis або CDN.
Ці інфраструктурні дії не змінюють семантичний стан бізнес-сутності і не порушують контракт безпеки.
М’яке видалення (Soft Delete)
У корпоративних додатках фізичне видалення (DELETE FROM table) часто замінюється логічним:
UPDATE users SET is_deleted = true, deleted_at = NOW() WHERE id = 42;
Навіть при м’якому видаленні метод DELETE залишається ідемпотентним: скільки б разів клієнт не відправляв запит, запис збереже статус is_deleted = true.
🔴 Senior Level
Ідемпотентність DELETE в Event-Driven Architecture (Kafka / Outbox)
У мікросервісних системах видалення сутності зазвичай супроводжується публікацією доменного повідомлення (події):
UserDeletedEvent(userId=42).
Якщо клієнт через мережевий збій повторює запит DELETE /api/v1/users/42, наївна реалізація може відправити другу подію в Kafka. Це призведе до дублювання подій у downstream-сервісах (білінг, нотифікації).
Реалізація з Transactional Outbox у 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()) {
// Ресурс уже видалено!
// НЕ генеруємо повторну подію в Outbox, зберігаючи ідемпотентність подій
return false;
}
User user = userOpt.get();
userRepository.delete(user);
// Публікуємо подію рівно один раз
OutboxEvent event = new OutboxEvent(
"USER_EVENTS",
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
}
// При повторному запиті повертаємо 404
return ResponseEntity.notFound().build(); // 404 Not Found
}
}
Умовний DELETE з Optimistic Locking (ETag / If-Match)
За специфікацією RFC 9110 для методу DELETE можна застосовувати заголовок If-Match:
DELETE /api/v1/documents/100 HTTP/1.1
If-Match: "v2-revision-etag"
Сервер перевіряє, що документ видаляється саме в тій версії, яку бачив користувач. Якщо інший користувач паралельно відредагував документ (версія стала v3), сервер поверне 412 Precondition Failed, захищаючи дані від випадкового видалення модифікованого документа.
Пакетне видалення (Bulk Delete): як не зламати REST
Поширена антипатерн-помилка: створення методу POST /api/v1/users/delete-batch.
Використання POST позбавляє операцію ідемпотентності на рівні протоколу.
Коректні REST-підходи:
- Query-параметри:
DELETE /api/v1/users?ids=1,2,3,4(методDELETEзберігає ідемпотентність). - Підпорядкований ресурс завдань: Якщо видалення мільйонів записів займає хвилини, використовується асинхронний патерн:
POST /api/v1/deletion-jobs->202 Accepted+ заголовокLocation: /api/v1/deletion-jobs/789.- Клієнт опитує статус завдання через ідемпотентний
GET /api/v1/deletion-jobs/789.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
GETідемпотентний, тому що є безпечним (Safe) методом: він семантично призначений тільки для читання і не мутує стан бізнес-ресурсів. Повторення запиту $N$ разів залишає базу даних у вихідному стані.DELETEідемпотентний, тому що приводить ресурс до детермінованого цільового стану: «ресурс видалено». Перший виклик видаляє об’єкт, усі повторні виклики констатують його відсутність.- Різниця в статус-кодах (
204 No Contentпри першому виклику і404 Not Foundпри повторному) не порушує ідемпотентність, оскільки стандарт RFC 9110 вимагає незмінності стану сервера, а не кодів відповідей.
4 каверзних питання з відповідями
-
Якщо
GET /articles/15збільшує лічильник переглядів у БД, чи перестає він бути Safe та ідемпотентним? Відповідь: З точки зору специфікації RFC 9110 §9.2.1, допоміжні технічні ефекти (логування, лічильники аналітики, кешування) не позбавляють метод статусу Safe, якщо вони не змінюють представлення цільового ресурсу і не впливають на бізнес-логіку управління ресурсом. Проте розміщувати критичну бізнес-логіку (наприклад, списання балансу) уGETкатегорично заборонено. -
Чому небезпечно робити операції видалення через
GET(наприклад,GET /users/1/delete)? Відповідь: Браузери, веб-краулери (пошукові роботи Google) та проксі-сервери вважаютьGETбезпечним методом. Пошуковий робот або браузерний механізм попереднього завантаження посилань (prefetching) перейде за цим посиланням і видалить дані без відома користувача. -
Що робити в Event-Driven архітектурі, якщо
DELETEвикликається повторно? Відповідь: Використовувати перевірку існування ресурсу перед публікацією події (або Transactional Outbox). ПовторнийDELETEповинен повернути клієнту404 Not Found(або204), але не повинен генерувати дублюючу подіюEntityDeletedу Kafka/RabbitMQ. -
Як реалізувати ідемпотентне пакетне видалення в REST API? Відповідь: Використовувати
DELETE /api/v1/items?id=1&id=2&id=3. ВикористанняPOST /api/v1/items/batch-deleteсемантично неідемпотентне і позбавляє клієнт можливості безпечного автоматичного повтору при збоях мережі.
Червоні прапорці (чого категорично не можна говорити)
- ❌ «DELETE не ідемпотентний, тому що повертає 404 замість 200/204 при повторі» (ідемпотентність оцінюється за станом БД, а не кодами відповідей).
- ❌ «GET запити заборонено логувати, тому що логування змінює стан сервера» (логування та збір метрик прямо дозволені RFC 9110).
- ❌ «Для пакетного видалення потрібно завжди робити POST» (для ідемпотентного пакетного видалення використовується
DELETEзі списком ID в query). - ❌ «Soft Delete робить метод DELETE неідемпотентним» (навпаки, повторний UPDATE одного й того самого прапорця
is_deleted = trueабсолютно ідемпотентний).