Клиент отправил POST для создания заказа, не дождался ответа из-за таймаута и повторил запрос. Как правильно не допустить второго заказа?
- Как защититься от двойного заказа при повторе запроса?
- Как работает Idempotency-Key?
Клиент генерирует ключ идемпотентности (UUID) на операцию и передаёт его в заголовке; сервер сохраняет ключ и результат в одной транзакции с созданием заказа и на повтор возвращает сохранённый ответ.
- Уникальный индекс по ключу защищает от параллельных повторов
- Тот же ключ с другим телом — ошибка 422, а не молчаливый повтор
- Ключи хранят ограниченное время (сутки–неделя)
А ещё Та же идея для сообщений из брокера: таблица обработанных id
Подробно
POST не идемпотентен, а клиент после таймаута не знает, создан ли заказ. Повторить нельзя — будет два заказа; не повторить — заказ может потеряться. Решение — сделать операцию идемпотентной явно.
Схема:
- клиент генерирует UUID для конкретного намерения («оформить эту корзину») и отправляет
Idempotency-Key: <uuid>. При повторе — тот же ключ; - сервер в одной транзакции: вставляет ключ в таблицу (уникальный индекс), создаёт заказ, сохраняет ответ рядом с ключом;
- если ключ уже есть — возвращает сохранённый ответ, не создавая заказ;
- если запрос с этим ключом ещё выполняется (параллельный повтор) — ждать или ответить 409 «в обработке».
Критичные детали:
- ключ и бизнес-изменение — в одной транзакции, иначе можно упасть между ними и получить заказ без ключа (или ключ без заказа);
- проверять, что тело совпадает с исходным: тот же ключ с другими данными — ошибка клиента;
- не сравнивать тела вместо ключа: два разных заказа могут совпасть по содержимому;
- срок хранения ключей — сутки или неделя, потом чистить.
Так работают API Stripe, ЮKassa и других платёжных систем. Для сообщений из брокера с доставкой «хотя бы один раз» — та же идея: id сообщения в таблице обработанных в одной транзакции с эффектом.
Пример
BEGIN;
INSERT INTO idempotency_keys(key, request_hash) VALUES ($1, $2); -- уникальный индекс
INSERT INTO orders(...) VALUES (...) RETURNING id;
UPDATE idempotency_keys SET response = $3 WHERE key = $1;
COMMIT;
-- повтор: INSERT упадёт по уникальности → SELECT response FROM idempotency_keys WHERE key = $1Что хотят услышать
- Ключ от клиента на намерение, а не на попытку.
- Сохранение ключа и результата в одной транзакции с операцией.
- Обработка параллельных повторов и несовпадающего тела.
- Перенос идеи на обработку сообщений.
Частые ошибки
- Генерировать ключ на сервере или на каждую попытку.
- Дедуплицировать по совпадению содержимого запроса.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✗Заменить POST на PUT — тогда повтор станет безопасным сам по себе
- ✗Отключить ретраи на клиенте и увеличить таймаут
- ✗Сравнивать тела запросов и отклонять полностью совпадающие в течение минуты
- ✓Клиент шлёт Idempotency-Key, сервер хранит его с результатом и на повтор отдаёт тот же ответ
Что спросят дальше
- Сколько хранить ключи идемпотентности?
- Как та же идея работает при at-least-once доставке из брокера?
Разбор темы
- Проектирование API: REST, gRPC и события
Выбор стиля взаимодействия, семантика и идемпотентность HTTP, формат ошибок, курсорная пагинация против offset, версионирование и обратная совместимость, контрактное тестирование.
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.