Idempotency keys: 5 граблей на проде платёжного шлюза
Пятница, 23:47. Алерт: платёж AmEx провалился три раза подряд, провайдер вернул 5xx. Закрыл как временный сбой. Через 40 минут — скрин выписки клиента: три списания за одну бронь. У него рейс через 6 часов. Мы делали B2B-платформу для деловых поездок: бронь авиа, отели, трансфер, оплата корпоративной картой. С той ночи началась история, которая закончилась переписыванием всего платёжного слоя. По дороге поймали 5 граблей с ключами идемпотентности. Грабли №1: Провайдер вернул 5xx, но списал деньги. Мы полагались, что 5xx = транзакция не прошла. На деле провайдер иногда списывал, но не успевал отдать 2xx из-за таймаута. Решение: добавили reconciliation job, сверяем списки транзакций раз в 10 минут. Грабли №2: Генерили idempotency key на фронте. Если клиент обновлял страницу, новый ключ — новый платёж. Перенесли генерацию на бэк, привязали к идентификатору брони. Один ключ на весь lifecycle операции. Грабли №3: TTL ключей 24 часа — мало для B2B. Корпоративные карты часто блокируют транзакцию на проверку. Клиент повторял через 2 дня — дубль списания. Подняли TTL до 30 дней, добавили логику продления для активных операций. Грабли №4: Race condition при одновременных запросах. Два воркера обрабатывали один ключ параллельно. Добавили distributed lock в Redis с timeout 30 секунд. Если не получили лок — 409 Conflict с retry-after. Грабли №5: Логи не связывали все попытки. Три записи с разными request_id, найти историю — квест. Ввели correlation_id из idempotency key, прокидываем через все сервисы. Теперь вся цепочка в одном trace. Итог: переписали платёжный слой за месяц, запустили на 15% траффика, через неделю раскатали на всех. Дублей списаний нет уже полгода. Главный вывод: idempotency keys — не просто UUID в заголовке, это отдельная подсистема со своим lifecycle и мониторингом. Какие грабли с идемпотентностью ловили вы? Особенно интересно про специфику payment gateways и банковские API.