← Назад к списку
Системный дизайнАрхитектура и алгоритмыMiddle

Клиент отправил запрос на списание денег, не дождался ответа и повторил его. Как спроектировать идемпотентность API, чтобы списание не прошло дважды?

Короткий ответ

  • Сетевой таймаут не говорит, выполнился запрос или нет
  • Клиент генерирует idempotency key на операцию, не на попытку
  • Сервер хранит ключ со статусом и результатом операции
  • Повтор с тем же ключом возвращает сохранённый результат
  • Проверка и вставка ключа — атомарно, уникальным индексом
  • Конкурентный повтор во время обработки получает 409 или ждёт
  • TTL ключей и одинаковое тело запроса при повторе

Идемпотентность строится на клиентском ключе операции и атомарной записи его на сервере: первый запрос выполняется, повторы получают сохранённый результат.

Как сказать вслух

пример ответа

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

Подробный ответ

Основной ответ

Клиент генерирует UUID операции (idempotency key) один раз — например, при нажатии «Оплатить» — и передаёт его в заголовке со всеми ретраями. Сервер при получении пытается атомарно вставить ключ в таблицу с уникальным индексом и статусом processing. Если вставка прошла — выполняет операцию, сохраняет результат (код и тело ответа) рядом с ключом и отвечает. Если ключ уже существует: статус completed — вернуть сохранённый ответ; статус processing — конкурентный повтор, ответить 409/425 или подождать завершения. Ключи хранятся с TTL (часто 24 часа). Важно, чтобы повтор шёл с тем же телом: расхождение — ошибка 422. Такая схема превращает at-least-once ретраи в exactly-once эффект.

Ключевые моменты

  • Ключ на операцию. Ключ создаётся один раз на бизнес-действие и переиспользуется во всех ретраях; новый ключ на каждую попытку обнуляет защиту.
  • Атомарная фиксация. Уникальный индекс в БД решает гонку двух одновременных запросов: вставится только один, второй увидит конфликт.
  • Сохранение результата. Хранится не факт «было», а полный ответ — повтор получает тот же результат, включая ошибки бизнес-логики.
  • Статус processing. Окно, пока операция выполняется, — отдельный случай: повтор не должен ни выполнить её второй раз, ни получить ложный отказ.

Практический контекст

Это любимый вопрос в финтехе и e-commerce. Интервьюер проверяет: понимаете ли вы, что GET/PUT/DELETE идемпотентны по семантике, а POST — нет; увидите ли гонку конкурентных повторов; вспомните ли про то, что вызов внешнего платёжного шлюза тоже нужно делать с его ключом идемпотентности. Уточните: кто источник ключа, сколько его хранить, что делать при повторе с изменённым телом.

Частые ошибки

  • Проверяют существование ключа SELECT-ом, а потом вставляют — гонка двух запросов проходит обе проверки
  • Генерируют ключ на каждую HTTP-попытку вместо одной бизнес-операции
  • Делают идемпотентным свой API, но зовут внешний платёжный шлюз без его ключа идемпотентности

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru