Проблема: клієнт відправив POST /payments, а відповідь не дійшла - таймаут, обрив мережі. Чи пройшов платіж? Повторити запит небезпечно (подвійне списання), не повторити - теж (платіж може не пройти).
Рішення: клієнт генерує унікальний ключ (UUID) для кожної логічної операції і передає його в заголовку. Повтор з тим самим ключем не виконує операцію вдруге, а повертає збережену відповідь першого виконання.
POST /payments
Idempotency-Key: 4f0f9c2e-8b1a-4c3e-9d5f-2a7b6c8d9e01
{"amount": 5000, "currency": "UAH"}
Реалізація на сервері:
- Отримати ключ і атомарно зарезервувати його (унікальний індекс у таблиці чи
SET NXу Redis) разом з ID користувача. - Якщо ключ новий - виконати операцію й зберегти статус і тіло відповіді.
- Якщо ключ уже є й операція завершена - повернути збережену відповідь.
- Якщо ключ є, але операція ще виконується (паралельний повтор) -
409 Conflict, щоб клієнт повторив пізніше. - Якщо той самий ключ прийшов з іншим тілом запиту -
422: це помилка клієнта, ключ використано повторно для іншої операції.
Деталі, про які питають:
- Ключ прив'язують до користувача, щоб чужий ключ не дав доступ до чужої відповіді.
- Термін зберігання ключів - зазвичай 24 години.
- Зберігати відповідь потрібно в тій самій транзакції, що й результат операції, інакше падіння між ними знову дасть подвійне виконання.
- Помилки валідації (4xx) зазвичай зберігають, а тимчасові збої (5xx) - ні, щоб повтор мав шанс пройти.
Саме так працюють Stripe і більшість платіжних API. IETF стандартизує заголовок Idempotency-Key в окремій специфікації.