Электронный кошелёк: что в платёжных системах важнее кода
Дважды в карьере я строил электронный кошелёк — в 2010 и почти десять лет спустя. Технологии сменились полностью, а главные уроки остались теми же.
Эта запись есть на другом языке: English
Первый электронный кошелёк я начал делать в 2010 году. Потом были платёжная система, кабинет дилеров, приложения для платежей. А почти десять лет спустя, уже в телекоме, я снова оказался перед той же задачей — проектировать кошелёк и платёжную инфраструктуру для абонентов.
Между этими двумя точками сменилось всё: языки, фреймворки, подход к архитектуре, способ деплоя. Не изменилось одно — список вещей, на которых платёжные системы ломаются. Он оказался на удивление устойчивым, и почти ничего в нём не касается кода напрямую.
Деньги — это не число в базе
Самая частая ошибка на старте — представить баланс как поле в таблице пользователя и менять его командой UPDATE.
Так делать нельзя, и дело не в производительности. Число в колонке не отвечает на вопрос «почему у человека именно столько денег». А этот вопрос вам зададут — пользователь, бухгалтерия, регулятор, и в самый неудобный момент.
Правильная модель — журнал операций, из которого баланс вычисляется. Каждое движение денег — отдельная неизменяемая запись: откуда, куда, сколько, когда, по какому основанию. Баланс — это производная величина, а не источник истины.
Отсюда следует несколько правил, которые лучше принять сразу:
- Ничего не удалять. Ошибочная операция не стирается — она компенсируется обратной проводкой. История должна оставаться полной.
- Ничего не редактировать задним числом. Исправление — это новая запись, а не правка старой.
- Хранить всё, что объясняет решение. Курс, комиссию, идентификатор внешней транзакции, ответ провайдера. Через полгода без этого не разобраться.
Такой журнал кажется избыточным ровно до первого разбирательства. После него он кажется единственным разумным вариантом.
Идемпотентность — главный урок
Если из всей статьи запомнить один термин, пусть будет этот.
Сеть ненадёжна. Запрос ушёл, ответ потерялся. Мобильное приложение не дождалось и повторило. Пользователь увидел «ошибка» и нажал кнопку ещё раз. Внешняя система таймаутнулась и переотправила уведомление.
Во всех этих случаях деньги не должны списаться дважды.
Решение концептуально простое: у каждой операции есть уникальный ключ, который генерирует инициатор. Повторный запрос с тем же ключом не создаёт новую операцию, а возвращает результат уже существующей. Не «ошибка, дубликат», а именно тот же самый ответ, что и в первый раз.
Звучит тривиально, но на практике это требование расползается по всей системе: по API, по обработчикам уведомлений, по фоновым задачам, по ретраям. Идемпотентность нельзя «добавить потом» — её либо закладывают в модель с самого начала, либо переписывают половину системы.
Состояние платежа — это конечный автомат
Платёж не бывает «прошёл» или «не прошёл». Между ними живёт самое интересное состояние: неизвестно.
Мы отправили запрос в банк и не получили ответа. Деньги, возможно, списались. Возможно, нет. Пока мы не выясним точно, операция не завершена и не отменена — она висит.
Поэтому у платежа должен быть явный набор состояний и явные правила переходов между ними: создан → в обработке → успешен / отклонён / требует выяснения. И обязательно — механизм, который разбирается с «зависшими»: повторный опрос статуса у провайдера, таймаут, автоматическая отмена или эскалация человеку.
Система, в которой нет состояния «не знаю», рано или поздно либо теряет деньги, либо создаёт их из воздуха.
Сверки — не опция
Как только появляется вторая система — банк, процессинг, оператор, партнёр — появляются и две версии правды. Ваша база говорит одно, их выгрузка другое. Не потому что кто-то жульничает, а потому что часть операций пришлась на сбой, таймаут или ночное окно обслуживания.
Расхождения будут всегда. Вопрос только в том, узнаете вы о них через сутки из автоматической сверки или через месяц от бухгалтерии.
Поэтому регулярная сверка — такая же обязательная часть платёжной системы, как сама обработка платежей. И к ней нужен понятный инструмент: не «выгрузить два CSV и сравнить в Excel», а нормальный отчёт о расхождениях с возможностью разобрать каждое.
Наблюдаемость важнее, чем кажется
В споре «у меня списались деньги, а услуга не пришла» выигрывает не тот, кто прав, а тот, у кого есть лог.
В платёжной системе нужно хранить фактический обмен с внешними системами: что именно отправили, что именно получили, когда. Это спасает и в разборе инцидентов, и в переговорах с партнёром, чья документация расходится с поведением их API — а расходится она почти всегда.
Отдельно стоит завести алерты не только на ошибки, но и на тишину: если поток платежей внезапно упал до нуля, это плохая новость, даже если в логах нет ни одного исключения.
Самое сложное — не технологии, а доверие
Техническая часть платёжной системы сложная, но конечная. Гораздо тяжелее другое.
Когда у человека пропадают деньги — пусть на десять минут, пусть из-за задержки провайдера — он не думает про архитектуру. Он думает, что его обманули. И восстанавливать это доверие сильно дороже, чем предотвратить сбой.
Из этого следуют вещи, которые инженеру не всегда очевидны:
- Понятные статусы вместо технических ошибок. «Платёж обрабатывается, деньги вернутся в течение часа, если не пройдёт» лучше, чем «Error 500».
- Поддержка должна видеть то же, что и система. Если оператор не может ответить «где мои деньги», система спроектирована плохо, какой бы красивой она ни была внутри.
- Предсказуемость важнее скорости. Платёж, который всегда проходит за десять секунд, лучше платежа, который обычно мгновенный, но иногда зависает на сутки.
Что я вынес из двух подходов к одной задаче
Между кошельком 2010 года и кошельком 2019-го разница в технологиях огромная. Но если бы меня спросили, что действительно определило результат в обоих случаях, я бы назвал не стек.
Определяли модель данных (журнал против числа в колонке), отношение к сбоям (закладываем их в дизайн или надеемся, что пронесёт) и готовность объяснить каждую копейку через полгода после того, как всё произошло.
Это, пожалуй, главное отличие финтеха от обычной разработки. В большинстве систем ошибка означает неудобство. Здесь она означает чужие деньги — и это меняет приоритеты целиком.