АНАТОМИЯ ФИЧИ: Как Stripe не списывает деньги дваждыКлиент отправил платёж, а ответа нет: оборвалась сеть или сработал timeout. Списались деньги или нет, неизвестно, а повтор может списать их второй раз.
Идемпотентность – свойство операции: повторный вызов даёт тот же эффект, что первый, и деньги не спишутся дважды. Для этого клиент кладёт в каждый запрос уникальный ключ идемпотентности. Когда запрос приходит повторно, сервер видит знакомый ключ и понимает, что это дубль, а не новый платёж.
🔬 **Что видно снаружи**
В основном API Stripe (v1) ключ передают в заголовке
Idempotency-Key.Повтор с тем же ключом получает тот же код и то же тело ответа, даже если в первый раз был
500.⚙️ **Что сервер запоминает по ключу**
Stripe хранит не факт «запрос был», а результат первого вызова, который дошёл до выполнения.
Повтор пришёл, пока первый ещё выполняется, – ответ
409 Conflict. С тем же ключом пришли другие параметры – ошибка.Слой перед бизнес-логикой ищет ключ в хранилище. Нет ключа – ставит запись с блокировкой, выполняет платёж и сохраняет ответ. Есть – отдаёт сохранённое.
🧬 **Почему ключ даёт клиент и чем за это платят**
Сервер не отличит повтор от второго настоящего платежа: запросы одинаковые. Только клиент знает, отправил он тот же платёж ещё раз или хочет заплатить второй раз, поэтому ключ генерирует он.
Хранить ответы вечно дорого: по документации ключ можно удалить, когда ему исполнилось 24 часа, и повтор после этого создаст новый платёж.
Платит интегратор. Ключ записывайте до отправки: сервис упал, после рестарта сделал новый ключ, и защиты нет.
Повтор после
500 вернёт тот же 500, поэтому что случилось с платежом, приходится выяснять отдельно.🤝 **Та же задача без заголовка-ключа**
🔹 Kafka, idempotent producer: брокер помнит ID продюсера и номер сообщения в партиции и отбрасывает дубли. Номер ставит сама система, но защита живёт одну сессию продюсера.
🔹
PUT на ресурс с ID, который выбрал клиент: повтор записывает то же состояние, ответы хранить не нужно. Подходит, пока операция звучит как «установить», а не «сделать».🔹 Уникальный индекс в базе:
INSERT ... ON CONFLICT DO NOTHING по номеру заказа. Дубль отсекается по бизнес-ключу, но первый ответ повтору никто не вернёт.Все варианты решают одну задачу: узнать повтор. Для этого у операции нужна метка, одинаковая у оригинала и у повтора, и система должна помнить её, пока повтор ещё может прийти.
В Stripe метку придумывает клиент – это ключ, и сервер помнит его не меньше суток. В Kafka метку ставит сама система – номер сообщения, но помнит её только до перезапуска продюсера. В
PUT метка – ID ресурса, в уникальном индексе – номер заказа, и обе живут, пока лежит запись.Когда проектируют идемпотентность своего API, решают именно эти два вопроса: кто ставит метку и сколько её хранить. Заголовок
Idempotency-Key – лишь один из способов передать метку.📚 **Что почитать:**
"
Idempotent requests" – документация Stripe API"
Implementing Stripe-like Idempotency Keys in Postgres" – Brandur Leach, 2017