Код-ревью Senior Транзакции и @Transactional обновлено
Код-ревью: оплата заказа
Вопрос
Найдите проблемы с транзакциями.
@Service
public class PaymentService {
private final OrderRepository orders;
private final PaymentGateway gateway;
private final AuditRepository audit;
private final MailSender mail;
public PaymentService(OrderRepository orders, PaymentGateway gateway,
AuditRepository audit, MailSender mail) {
this.orders = orders;
this.gateway = gateway;
this.audit = audit;
this.mail = mail;
}
@Transactional
public void pay(long orderId) throws PaymentException {
Order order = orders.findById(orderId).orElseThrow();
order.setStatus(Status.PAYING);
GatewayResult r = gateway.charge(order.getTotal());
if (!r.ok()) {
writeAudit(orderId, "declined");
throw new PaymentException("declined");
}
order.setStatus(Status.PAID);
mail.send(order.getEmail(), "Оплачено");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAudit(long orderId, String text) {
audit.save(new AuditRecord(orderId, text));
}
@Transactional
private void refund(Order order) {
gateway.refund(order.getTotal());
order.setStatus(Status.REFUNDED);
}
}
Коротко
Платёж это операция с внешним миром, поэтому одна большая транзакция здесь не подходит. Нужно: короткая транзакция «начали оплату», вызов шлюза вне транзакции с ключом идемпотентности, вторая транзакция с результатом, аудит в отдельном бине с REQUIRESNEW, письмо после коммита.
Что должен найти ревьюер
- Ошибкастрока 20Внешний платёжный вызов внутри транзакции: соединение из пула держится всё время ответа шлюза; при откате после успешного списания деньги потеряны без записи.
- Ошибкастрока 22writeAudit вызывается через this: REQUIRES_NEW не сработает, запись аудита в той же транзакции.
- Ошибкастрока 23PaymentException checked (объявлен в throws): по умолчанию транзакция закоммитится, статус PAYING и аудит сохранятся. Нужен rollbackFor или осознанное решение.
- Ошибкастрока 26Письмо уходит до коммита: если коммит упадёт, клиент получит «Оплачено» без оплаты. Нужен AFTER_COMMIT слушатель или outbox.
- Ошибкастрока 35@Transactional на private-методе игнорируется прокси.
Разбор
Платёж это операция с внешним миром, поэтому одна большая транзакция здесь не подходит. Нужно: короткая транзакция «начали оплату», вызов шлюза вне транзакции с ключом идемпотентности, вторая транзакция с результатом, аудит в отдельном бине с REQUIRES_NEW, письмо после коммита. И явно решить, что делать с checked-исключением.
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.