Короткий ответ
Идемпотентность платежа строят на клиентском idempotency key, привязанном к конкретной попытке оплаты, с атомарной резервацией ключа до начала обработки; сложность возникает при одновременных запросах с одним ключом от разных источников — здесь нет единственно верной реализации блокировки «в процессе».
Как это работает подробнее
При получении запроса с idempotency key сервис атомарно регистрирует попытку в статусе «in_progress» перед обращением к провайдеру; при повторном запросе с тем же ключом, пока первая попытка ещё «in_progress», вторая параллельная попытка оплаты не должна начинаться.
Синхронная блокировка второго запроса до завершения первого даёт простую модель согласованности, но увеличивает латентность ответа и риск таймаута у клиента; асинхронная модель «processing + polling» не блокирует клиента надолго, но требует поддержки опроса статуса на стороне потребителей API. Выбор зависит от контроля над клиентским кодом и критичности быстрой обратной связи по результату оплаты.
Пример кода
При status = 'in_progress' и повторном запросе -> не начинать новую попытку к провайдеру,
а сверяться с provider_tx_id через API провайдера, если прошло больше N секунд без обновления.Что использовать на практике
- Практика. Варианты для конкурентного повтора — заблокировать второй запрос до завершения первого либо вернуть статус «processing» и предложить клиенту опросить результат позже; оба должны корректно обрабатывать случай, когда первая попытка «зависла» без ответа провайдера.
Подводные камни
- Риск. Считать, что одного уникального ограничения в БД достаточно — оно защищает от дублирования записи, но не от того, что сервис дважды обратится к провайдеру, если оба запроса прошли проверку до фиксации статуса; не предусматривать путь разрешения «неопределённого» исхода через сверку с провайдером.