Клиент отправил запрос на списание денег, не дождался ответа и повторил его. Как спроектировать идемпотентность 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, но зовут внешний платёжный шлюз без его ключа идемпотентности