Кандидат говорит: «если запрос к внешнему сервису падает по таймауту, ставим @Retryable и дело сделано». Собеседующий кивает и спрашивает: а что, если первый запрос дошёл, но ответ потерялся по дороге?
Это ключевая развилка. Таймаут — это не отказ, это неизвестность. Сервис мог успеть выполнить операцию и не успеть ответить. Если метод неидемпотентен (например, списание баланса или отправка email), повтор задублирует эффект, даже если с точки зрения вашего кода всё выглядит правильно: упало — повторили — получилось.
@Retryable( retryFor = TimeoutException.class, maxAttempts = 3, backoff = @Backoff(delay = 500) ) public void chargeCustomer(String orderId, BigDecimal amount) { paymentGateway.charge(orderId, amount); }
Добавить @Retryable сюда — ничего не изменить по сути. Нужен идемпотентный ключ на стороне платёжного шлюза, который гарантирует: повторный вызов с тем же ключом не создаст второе списание, а вернёт результат первого.
Следом спросят: а если платёжный шлюз сам не поддерживает идемпотентные ключи? Тогда перед ретраем нужно сначала проверить статус операции отдельным запросом (idempotent by design на вашей стороне через таблицу с уникальным constraint на order_id), и только если статус неизвестен или failed — повторять. Ретрай вслепую без проверки факта — это то, что роняет кандидатов на этом вопросе.
Ретрай и идемпотентность решают разные проблемы: один борется с сетевыми сбоями, вторая — с последствиями этой борьбы.
senior·base — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки