Коротко: идемпотентность — операция даёт один результат при любом числе повторов; для API критична, иначе повторный запрос спишет деньги дважды.

Идемпотентность — это свойство операции давать один и тот же результат, сколько бы раз её ни повторили. Для API это критично: сети ненадёжны, клиенты ретраят запросы, и без идемпотентности повторный «создать платёж» легко спишет деньги дважды. Разберём, какие методы идемпотентны по природе, и как сделать идемпотентным то, что им не является.

Почему это важно

Классический сценарий: клиент отправил POST /payments, сервер обработал и списал деньги, но ответ потерялся в сети (таймаут). Клиент не знает, прошёл ли запрос, и повторяет его. Без защиты — второе списание. С идемпотентностью — повтор вернёт результат первого запроса, не создавая дубль.

Идемпотентность HTTP-методов

  • GET, HEAD — идемпотентны и безопасны (ничего не меняют).
  • PUT — идемпотентен: «положить ресурс в состояние X» повторно даёт то же состояние.
  • DELETE — идемпотентен: удалить уже удалённое — состояние то же (обычно 404/204).
  • POSTне идемпотентен по умолчанию: каждый вызов создаёт новый ресурс. Именно его и нужно защищать.
  • PATCH — зависит от семантики (инкремент +1 не идемпотентен, установка поля — идемпотентна).

Idempotency-Key — стандартный приём

Клиент генерирует уникальный ключ на каждую логическую операцию и шлёт его в заголовке. Сервер запоминает ключ и результат; при повторе с тем же ключом возвращает сохранённый ответ, не выполняя операцию снова.

POST /payments
Idempotency-Key: 5f3c9a1e-...-clientgenerated
Content-Type: application/json

{ "amount": 1000, "currency": "RUB" }

Так делают Stripe, платёжные шлюзы и зрелые API. Ключ обычно живёт ограниченное время (например, 24 часа).

Серверная логика обработки ключа

public function createPayment(string $idempotencyKey, array $data): Response
{
    // 1) уже видели такой ключ? вернуть сохранённый результат
    if ($saved = $this->store->find($idempotencyKey)) {
        return $saved->response;
    }

    // 2) атомарно «застолбить» ключ (уникальный индекс в БД)
    //    при гонке двух одинаковых запросов второй упрётся в дубликат
    try {
        $this->store->reserve($idempotencyKey);
    } catch (DuplicateKeyException) {
        return $this->waitOrReturnInProgress($idempotencyKey);
    }

    // 3) выполнить операцию и сохранить результат под ключом
    $result = $this->processPayment($data);
    $this->store->save($idempotencyKey, $result);

    return $result;
}

Ключевой момент — атомарность: «застолбить» ключ нужно через уникальный индекс или блокировку, иначе два одновременных повтора оба пройдут проверку шага 1.

Где ещё нужна идемпотентность

  • Очереди сообщений (Kafka/RabbitMQ) — доставка «at least once» означает, что сообщение может прийти дважды. Обработчик обязан быть идемпотентным (по message-id или бизнес-ключу).
  • Вебхуки — внешние сервисы ретраят доставку; обрабатывайте по event-id.
  • Фоновые задачи — воркер может упасть и перезапустить задачу.

Приёмы обеспечения идемпотентности

  • Idempotency-Key + хранилище результатов (описано выше).
  • Естественный уникальный ключ: уникальный индекс на бизнес-поле (например, номер заказа) — повторный INSERT упрётся в дубликат.
  • Upsert (INSERT ... ON DUPLICATE KEY UPDATE / ON CONFLICT) вместо «проверил-вставил».
  • Проверка состояния: «если платёж уже в статусе paid — ничего не делать».

Вывод

Идемпотентность — не «фича для красоты», а базовое требование к надёжному API и обработчикам сообщений. Любую операцию, которую клиент может повторить (а он повторит), нужно проектировать так, чтобы повтор не навредил. Дешевле заложить это сразу, чем ловить двойные списания в проде.

Если проектируете платёжное или интеграционное API и хотите заложить идемпотентность и обработку повторов правильно — разберём на консультации.