Ревизия и улучшение функции денежного перевода на Kotlin

Перевод требует одной транзакции, защиты от конкурентного изменения обоих счетов и проверок суммы. Одна аннотация транзакции не устраняет lost update в схеме «прочитал — вычислил — записал».

Короткий ответ

Что ответить на собеседовании

Перевод требует одной транзакции, защиты от конкурентного изменения обоих счетов и проверок суммы. Одна аннотация транзакции не устраняет lost update в схеме «прочитал — вычислил — записал».

Подробный разбор

Ответ с пояснениями

Условие

В данном примере показана простая функция на Kotlin, осуществляющая перевод средств с одного счета на другой. Необходимо выявить потенциальные проблемы, связанные с транзакциями, и предложить способы её оптимизации или полной замены.

fun transfer(sourceAccountId: Long, destinationAccountId: Long, transferAmount: Int) {
    val sourceCurrentBalance = dao.getCurrentSum(sourceAccountId)
    dao.updateSum(sourceAccountId, sourceCurrentBalance - transferAmount)
    val destinationCurrentBalance = dao.getCurrentSum(destinationAccountId)
    dao.updateSum(destinationAccountId, destinationCurrentBalance + transferAmount)
}

Что может пойти не так?

  • Отсутствие проверки достаточности средств на исходном счёте → возможен отрицательный баланс.
  • Нет валидации входных параметров (отрицательная сумма, одинаковые идентификаторы).
  • Операции чтения и записи не находятся в одной атомарной транзакции → при конкурентных запросах может возникнуть состояние гонки и потеря средств.
  • Ошибки доступа к базе (исключения) не обрабатываются, что может привести к частичному выполнению операции.
  • Нет логирования и аудита действий, что усложняет отладку и мониторинг.

Варианты рефакторинга:

  1. Обернуть чтение и обновление балансов в единую транзакцию уровня SERIALIZABLE или использовать механизм optimistic locking.
  2. Добавить предварительные проверки: достаточность средств, корректность суммы, различие счетов.
  3. Ввести обработку исключений и откат транзакции в случае ошибки.
  4. Переписать функцию так, чтобы она возвращала результат операции (Result/Either) и/или бросала специализированные исключения.
  5. При необходимости вынести бизнес‑логику в отдельный сервис‑класс, а работу с базой оставить в DAO, что упростит тестирование (мок‑объекты).
  6. Добавить логирование операции и запись в аудиторскую таблицу.

Можно ли обойтись без этой функции? Если в системе уже реализован общий механизм финансовых операций (например, сервис платежей), то данную функцию следует удалить и использовать централизованный API, где уже реализованы все перечисленные проверки и транзакционность.

Ответ

Перевод требует одной транзакции, защиты от конкурентного изменения обоих счетов и проверок суммы. Одна аннотация транзакции не устраняет lost update в схеме «прочитал — вычислил — записал».

Практический вариант: проверить положительную сумму и разные счета, заблокировать обе строки в порядке их ID, проверить наличие счетов и достаточность средств, обновить оба баланса, записать операцию и зафиксировать транзакцию. Фиксированный порядок уменьшает риск deadlock. Деньги хранить целыми минимальными единицами с контролем переполнения либо подходящим decimal. Уникальный ключ операции предотвращает повторный перевод при retry. Ошибку нельзя проглатывать: нужен rollback; внешние уведомления отправляются через outbox после commit.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее