Что такое идемпотентность? Простой пример из жизни
Представьте, что вы нажимаете кнопку «Отправить заказ» в интернет-магазине. Если страница зависла, вы нажмёте её ещё раз. Идемпотентная система обработает повторный запрос так, что вы не получите два одинаковых заказа и с вашей карты не спишутся деньги дважды. Неидемпотентная — создаст два заказа и спишет средства дважды.
Идемпотентность — это свойство операции, позволяющее выполнять её многократно без изменения итогового результата после первого успешного выполнения. Ключевой акцент — на финальном состоянии системы.
Идемпотентность в HTTP: что говорят стандарты и как бывает на самом деле
Стандарт HTTP (RFC 7231) классифицирует методы, давая им ожидаемые свойства:
GET, HEAD, OPTIONS, TRACE — безопасные (не изменяют состояние) и, как следствие, идемпотентные.
PUT, DELETE — небезопасные, но идемпотентные. В этом главное архитектурное обещание протокола.
POST — небезопасный и неидемпотентный.
PATCH — небезопасный и, как правило, неидемпотентный (о нём подробнее ниже).
Пример на практике:
PUT /users/123 с телом
{"name": "Алексей"}.По стандарту: Идемпотентная операция «создать или полностью заменить» ресурс с ID 123. Сколько бы раз запрос ни повторился, пользователь 123 в итоге будет иметь имя «Алексей».
В реальности: Программист мог внутри PUT добавить логику увеличения счётчика изменений:
user.change_count += 1. Теперь каждый повторный запрос меняет состояние (счётчик растёт), ломая идемпотентность. Стандарт нарушен, но код работает.DELETE /cart/items/5.
По стандарту: Удалить товар 5 из корзины. После первого успешного выполнения товар удалён. Последующие вызовы должны возвращать ошибку 404 (Not Found), но состояние корзины (отсутствие товара 5) больше не меняется → операция идемпотентна.
В реальности: Программист мог сделать «мягкое у��аление» (флаг
deleted=true), и при каждом вызове запись будет помечаться удалённой снова. Это может быть идемпотентно, если логика корректна (установка флага вtrueповторно не меняет результат). А может и нет, если там есть побочные действия.
Особый случай: метод PATCH
Метод PATCH предназначен для частичного обновления ресурса. Его идемпотентность не гарантирована стандартом и полностью зависит от формата патча и его логики.
Неидемпотентный PATCH:
PATCH /balance { "operation": "add", "amount": 100 }. Каждое выполнение увеличит баланс на 100. Это типично для операций-инструкций.Идемпотентный PATCH:
PATCH /user { "name": "Мария" }. Установка имени в конкретное значение идемпотентна. Также идемпотентны форматы, описывающие изменение конкретного поля по определённому пути (JSON Patch при корректном использовании).
Почему идемпотентность критически важна? Не из-за RFC, а из-за жизни
Повторы от сетевых сбоев. Прокси-серверы, балансировщики, нестабильное соединение — всё это может автоматически отправить ваш запрос повторно. Без идемпотентности это приводит к дублированию платежей, заказов, сообщений.
Стратегии повтора (Retry) на клиенте. Надёжные клиенты при таймауте или ошибке 5xx повторяют запрос. Для POST (неидемпотентного) это опасно. Для PUT и DELETE (идемпотентных по договорённости) — безопасно, если сервер соблюдает договорённость.
Распределённые системы и согласованность. В микросервисной архитектуре, где возможны отказы и отложенные повторы, только идемпотентные операции могут гарантировать eventual consistency (согласованность в конечном счёте) без потерь или дублей.
Как добиться идемпотентности на практике (если вы — разработчик)
Поскольку встроенных гарантий нет, нужно строить их самим.
1. Использование Idempotency-Key (Ключ идемпотентности)
Это главный практический приём для неидемпотентных по природе операций (например, списание денег, создание заказа).
Как работает: Клиент генерирует уникальный ключ (UUID) и отправляет его в заголовке, например,
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000.Что делает сервер: При первом запросе с новым ключом выполняет операцию и сохраняет результат (ответ и статус) в хранилище (Кэш, БД), связав с ключом. При повторном запросе с тем же ключом не выполняет логику, а сразу возвращает сохранённый ответ.
Куда применять: Идеально подходит для критичных POST-запросов, совершающих действия.
2. Проверка последнего состояния (Оптимистичные блокировки)
Использование заголовков ETag и If-Match для обновления ресурсов. Если ресурс изменился с момента вашего последнего чтения, операция обновления не выполнится, предотвращая конфликт.
3. Подход «Создать, если не существует» для PUT. Реализация PUT как операции, которая создаёт ресурс с заданным идентификатором или полностью его заменяет, но без побочных эффектов (счётчиков, логов внутри операции).
Итог: принципы против реализа��ии
Принцип (RFC): PUT и DELETE идемпотентны. POST — нет. Это фундамент для построения предсказуемых сетевых взаимодействий.
Реальность (Код): Любой эндпоинт, обработанный как
app.post('/delete-item', ...), может быть идемпотентным. Любойapp.put('/increment', ...)— неидемпотентным.Вывод для разработчика: Всегда проектируйте свои API, следуя стандартам (это сделает вашу систему предсказуемой и надёжной). Но при интеграции со сторонними API или при анализе legacy-кода никогда не слепо доверяйте названию метода. Изучайте документацию, а если её нет — тестируйте поведение при повторах. Для критичных операций всегда используйте механизмы вроде Idempotency-Key на своей стороне.
Идемпотентность — это не просто академический термин, а практический инструмент для создания систем, которые не ломаются в условиях неидеальной сети и точно обрабатывают ваши данные.
Для глубокого погружения в тему идемпотентности в разных контекстах рекомендую подборку материалов на Habr: Идемпотентность: статьи на Habr.