41. Провести code review PaymentService
Условие задачи:
Дан Spring-сервис обработки платежей.
Необходимо провести code review, найти проблемы в реализации и предложить исправления.
Код:
@Service
public class PaymentService implements Status {
@Autowired
private PaymentRepository repo;
@Autowired
private KafkaTemplate<String, String> kafka;
@Transactional
public void processPayment(PaymentRequest request) {
Payment payment = new Payment();
payment.setAmount(request.getAmount());
payment.setStatus("NEW");
repo.save(payment);
try {
kafka.send(
"payments",
new ObjectMapper()
.writeValueAsString(payment)
);
} catch (Exception e) {
e.printStackTrace();
}
payment.setStatus("SENT");
repo.save(payment);
}
Integer getStatus() {
new RuntimeException();
}
}
Спойлеры к решению
Подсказки
ObjectMapper.💡 Отправка через
KafkaTemplate.send() выполняется асинхронно.💡 БД и Kafka не участвуют в одной обычной
@Transactional.💡 Посмотри, когда устанавливается статус
SENT.💡 Проверь реализацию
getStatus().Решение
Основные проблемы:
- используется field injection вместо constructor injection;
ObjectMapperсоздаётся при каждом вызове;KafkaTemplate.send()асинхронный, поэтомуtry/catchне гарантирует обработку ошибки отправки;- даже при неуспешной отправке платеж получает статус
SENT; e.printStackTrace()не подходит для серверного приложения;- транзакция БД не делает сохранение в БД и отправку в Kafka одной атомарной операцией;
- сообщение может быть отправлено до успешного commit транзакции;
- статус лучше хранить через
enum, а не строкой; - второй
repo.save(payment)обычно не нужен для managed entity внутри транзакции; getStatus()не возвращает значение;new RuntimeException()только создаёт исключение, но не выбрасывает его.
Исправление базовых проблем:
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentService {
private final PaymentRepository repo;
private final KafkaTemplate<String, String> kafka;
private final ObjectMapper objectMapper;
@Transactional
public void processPayment(PaymentRequest request) {
Payment payment = new Payment();
payment.setAmount(request.getAmount());
payment.setStatus(PaymentStatus.NEW);
repo.save(payment);
try {
String message =
objectMapper.writeValueAsString(payment);
kafka.send("payments", message);
payment.setStatus(PaymentStatus.SENT);
} catch (JsonProcessingException e) {
log.error(
"Failed to serialize payment {}",
payment.getId(),
e
);
throw new IllegalStateException(
"Failed to serialize payment",
e
);
}
}
}
Однако даже такой вариант не решает главную проблему согласованности:
БД commit
+
Kafka send
не являются одной транзакцией.
Например:
payment сохранён в БД
↓
Kafka недоступна
↓
сообщение потеряно
Или наоборот — сообщение может уйти, а транзакция БД затем откатится.
Для production-сценария обычно применяют Transactional Outbox: вместе с Payment в одной DB-транзакции сохраняется событие в outbox-таблицу, а отдельный процесс публикует его в Kafka.
Метод:
Integer getStatus() {
new RuntimeException();
}
не компилируется, поскольку отсутствует return.
Если нужно выбросить исключение:
public Integer getStatus() {
throw new RuntimeException();
}
Если метод должен возвращать статус, необходимо вернуть соответствующее значение согласно контракту интерфейса Status.