Кейс: скидочный купон применился к заказу дважды при повторном клике
Пользователь на медленном мобильном интернете жал кнопку «применить купон» два раза подряд, не видя мгновенной реакции интерфейса. Оба запроса долетали до backend с разницей в сотни миллисекунд.
Сервис проверял купон так: читал текущую скидку заказа, если скидки нет — применял купон и уменьшал сумму заказа на процент. Оба запроса читали заказ до того, как первый успевал записать применённую скидку — оба видели «скидки нет» и оба применяли купон.
В логах это выглядело безобидно: два одинаковых запроса, оба вернули 200. Проблему нашли только когда бухгалтерия заметила заказы со скидкой больше максимально разрешённой.
@Transactional public void applyCoupon(Long orderId, String code) { Order order = orderRepo.findById(orderId) .orElseThrow(); if (order.getDiscount() == null) { order.applyDiscount(code); orderRepo.save(order); } }
@Transactional изолирует транзакцию от других транзакций по правилам уровня изоляции, но не превращает read-then-write в атомарную операцию сама по себе — обе транзакции на Read Committed спокойно читают одинаковое стартовое состояние.
Починили через уникальный constraint на пару (order_id, coupon_code) в таблице применённых купонов плюс идемпотентный ключ на уровне API — второй запрос с тем же ключом идемпотентности возвращает результат первого, не выполняя логику повторно.
Двойной клик — не редкий баг фронтенда, а обычный сценарий, который backend обязан переживать.
Тренажёр: 600 вопросов, мок с таймером, план повторов
senior·base — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки