Кейс: скидочный купон применился к заказу дважды при повторном клике

Пользователь на медленном мобильном интернете жал кнопку «применить купон» два раза подряд, не видя мгновенной реакции интерфейса. Оба запроса долетали до 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 — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки