Пятница, 18:45. Вы уже мысленно закрываете ноутбук и строите планы на выходные. Вдруг в канале дежурной смены загорается алерт: Acceptance Rate (конверсия успешных оплат) за последние пятнадцать минут просел на тридцать процентов, и деньги клиентов начали зависать. Следом в чат врывается служба поддержки с первыми возмущёнными сообщениями от клиентов. «Деньги с карты списали дважды, а в профиле статус заказа всё ещё „Ожидает оплаты“!»
Знакомо? Вы хотя бы раз настраивали приём платежей, подключали интернет-эквайринг или интегрировались с банковскими API (Application programming interface, разговор с банком на его условиях). Тогда у вас прямо сейчас наверняка сжалось сердце.
В обычном веб-сервисе программная ошибка: рядовая задача в трекере, пара замечаний на встрече и срочное исправление в понедельник. В финтехе цена сбоя другая: заблокированные средства клиентов, кассовый разрыв, ярость бухгалтерии при закрытии периода. Добавьте штрафы платёжных систем за превышение доли спорных операций (chargeback ratio выше 0,9%). Прямой финансовый убыток бизнеса налицо. Доверие клиентов и платёжных партнёров здесь не вернуть красивым извинением в соцсетях.
Дальше пойдёт инженерная изнанка платёжных систем. Разберём, где в реальности прячутся подводные камни и почему привычные паттерны веб-разработки здесь опасны. И главное — как проектировать архитектуру, которая держит удар в любых нештатных ситуациях.
Анатомия транзакции
Представим классический сценарий. Пользователь оплачивает подписку в мобильном приложении, находясь в вагоне метро, на эскалаторе, в лифте подземного паркинга, где связь держится на честном слове. Связь нестабильна: запрос ушёл, экран замер на 4 секунды. Что делает пользователь? Инстинктивно нажимает кнопку «Оплатить» ещё пару раз.
Что в этот момент происходит на бэкенде, если архитектура написана «по типовым шаблонам» и повторные нажатия никто не предусмотрел, потому что в учебниках про это молчат?
Первый запрос успевает уйти в банк-эквайер, но связь обрывается до ответа. Второй запрос приходит ровно в тот момент, пока первый висит в открытой транзакции базы данных, а третий прилетает, когда банк уже заблокировал сумму и бэкенд ещё не успел об этом узнать. Итог: три заморозки средств на одной карте. Заказ отклонён по таймауту, а банковский процессинг временно считает ваши запросы спам-атакой.
Двухэтапная оплата
Первая концептуальная ошибка новичков в финтехе: считать, что деньги с карты списываются мгновенно и одной операцией.
В платёжных системах стандарт надёжности строится на двух шагах. Сначала банк-эмитент проверяет баланс карты и временно замораживает нужную сумму (Hold). Доступный остаток уменьшается, но деньги пока остаются в банке покупателя. Окончательное списание (Capture) происходит, когда магазин подтвердил оказание услуги или собрал товар на складе. «Всё в порядке, переводите средства». Только после этого платёжная система — МИР, Visa или Mastercard — ведёт деньги между банками, а до этого момента заказ обязан жить в состоянии ожидания, а не в статусе оплаченного.
Зачем разделять один платёж на два шага? Всё дело в цене ошибки.
Если товар закончился или заказ отменился, заморозка снимается мгновенно и абсолютно бесплатно через операцию отмены блокировки (Reversal). Деньги на карте клиента снова доступны в ту же секунду, а бизнес не теряет комиссию за эквайринг. Списать средства сразу одной транзакцией — значит превратить любую отмену в полноценный возврат (Refund). Уплаченная банку комиссия сгорает, оборотный капитал замораживается. Расстроенный покупатель ждёт своих денег от одного до трёх рабочих дней.
sequenceDiagram
actor Client as Клиент
participant Shop as Сервер магазина
participant Acquirer as Банк-эквайер
participant Issuer as Банк клиента<br/>(сумма заблокирована)
Client->>Shop: 1. Оплата
Shop->>Acquirer: 2. Заморозка (Hold)
Acquirer->>Issuer: Hold
alt Заказ собран?
Shop->>Acquirer: 3a. Списание (Capture)
Acquirer->>Issuer: Деньги ушли
else Нет
Shop->>Acquirer: 3b. Отмена блока (Reversal)
Acquirer->>Issuer: Мгновенный разблок
endИдемпотентность
В распределённых системах сетевой сбой случается штатно. Самая грубая ошибка здесь — настроить наивные автоповторы при любом сбое сокета. Если банковский шлюз успел обработать платёж, но ответ потерялся в сети, второй запрос просто спишет деньги ещё раз.
Поэтому любая операция изменения баланса строится на принципе идемпотентности. Сколько бы раз один и тот же запрос ни прилетал в систему, средства списываются строго один раз, а результат остаётся неизменным.
Честная защита от повторных списаний собирается из трёх деталей. Ключевая — клиентский ключ операции (Idempotency-Key), детерминированный идентификатор платежа, который формируется на стороне инициатора: хэш от номер_заказа + user_id + сумма + валюта или сгенерированный UUIDv4. Бэкенд атомарно резервирует этот ключ при получении запроса — например, через Redis SET resource_key my_token NX PX 10000 или транзакционную строку в БД, и параллельные дубли с тем же ключом тут же отсекаются. Замыкает схему машина состояний: платёж с этим ключом сейчас в статусе «В обработке» (PROCESSING)? Повторный запрос получает ответ 409 Conflict или ожидает финала первой операции. Если платёж уже завершился успехом или ошибкой, система моментально возвращает сохранённый результат из базы. Повторно дергать API банка она даже не пытается.
Оркестрация цепочек
Реальная оплата редко ограничивается одним вызовом. Скорее всего, это длинная цепочка. Заблокировать баллы лояльности, заморозить сумму на карте, зарегистрировать фискальный чек в онлайн-кассе (требование 54-ФЗ / ОФД). И это лишь половина маршрута: дальше проводка в главной книге и чек покупателю с подтверждением, а связать такие шаги в одну транзакцию базы данных невозможно. Внешние сервисы банков и онлайн-касс не поддерживают распределённые протоколы блокировок.
Решением выступает оркестрируемый сценарий Saga. Специальный координатор ведёт операцию шаг за шагом. Если на этапе печати чека сервис онлайн-кассы вернул критическую ошибку, координатор запускает компенсирующие действия в обратном порядке. Он снимает заморозку средств в банке, возвращает списанные бонусные баллы и помечает заказ отклонённым. Деньги клиента не зависают. Система сама возвращается в согласованное состояние без ручного вмешательства техподдержки.
Журнал проводок и двойная запись
Худшее, что можно встретить в коде платёжного сервиса, — это наивный SQL-запрос, правящий балансом прямой перезаписью цифры, будто истории операций не существует:
UPDATE accounts SET balance = balance - 500 WHERE id = 42;Почему в финтехе это считается табу? Баланс ведь не числовая колонка, которую можно произвольно перезаписывать. Это вычисляемый результат всей истории совершённых операций.
Просто перезаписали цифру в базе. Доказать, почему в 14:03 на счёте было 10 000 рублей, а в 14:04 — на 500 меньше, уже невозможно. Для финансового аудита, налоговой инспекции и расследования сбоев это приговор.
Итог: сумма по дебету (1 000 ₽) строго равна сумме по кредиту (970 + 30 ₽) — так выглядит двойная запись.
Каждое движение денег отражается минимум двумя взаимосвязанными записями: по дебету одного счёта и по кредиту другого. Правило двойной записи железное: сумма дебетов всегда строго равна сумме кредитов.
Нельзя «просто списать комиссию» — это первое, чему учит двойная запись: вы уменьшаете транзитный счёт операции и увеличиваете счёт доходов платформы от эквайринга.
Хранилище операций строится по принципу журнала, в который можно только дописывать (Append-Only). Записи создаются один раз; удалять их или модифицировать задним числом категорически запрещено.
Если произошла ошибка или отмена, в журнал вносится новая корректирующая запись (сторно). Только так утренняя сверка с банком гарантированно сойдётся до последней копейки.
Интеграция с банками
Любой разработчик, подключавший платёжный шлюз, проходит через стадии отрицания, гнева и смирения.
Вам присылают банковскую документацию. Возможно, это специфический диалект ISO 8583 либо протокол SOAP с тяжеловесными XML-пакетами, задокументированный так, будто его никто не открывал со смены процессингового центра. Вы настраиваете интеграцию в тестовом контуре (Sandbox). Всё выглядит прекрасно. Запросы летают за 80 мс, все сценарии возвращают идеальный SUCCESS.
Но в боевом контуре (Production) начинаются сюрпризы.
Первая неожиданность: тестовая песочница банка не воспроизводит боевую нагрузку. Партнёрская среда не симулирует пиковый трафик, таймауты и ночные регламентные окна процессинга (End of Day, EOD). Реальные отказы карт клиентов вроде 05: Do Not Honor или 51: Insufficient Funds она воспроизводит редко, поэтому первый боевой день всегда оборачивается сюрпризом.
Вторая неожиданность: асинхронные уведомления не гарантированы. Полагаться исключительно на входящие вебхуки от платёжного шлюза — фатальная ошибка. Оборвалась сеть, сбросилась TLS-сессия, перезагрузился балансировщик — платёж в банке прошёл, а ваша система навсегда зависла в статусе «Ожидает оплаты».
И третья классика корпоративных API: HTTP 200 OK с текстом ошибки внутри. Сетевой запрос завершился благополучным кодом 200, а в теле ответа приехал XML с сообщением SYSTEM_MALFUNCTION_TIMEOUT, и формально придраться не к чему, хотя платёж-то не прошёл. Простые HTTP-клиенты считают такой вызов успешным.
Интеграционный слой проектируется из расчёта, что банк рано или поздно подведёт.
Когда банк деградирует и ответы превышают восемь секунд, предохранители (Circuit Breaker) «размыкают цепь», а маршрутизация (Smart Routing) ведёт поток платежей на резервного банка-эквайера. Система не копит зависшие запросы. Конверсия оплат сохраняется даже при авариях у партнёра.
Статус платежа не пришёл вебхуком вовремя. Фоновые воркеры опрашивают API банка по строгому расписанию с нарастающей паузой: спустя 30 секунд, 2 минуты, 10 минут, 1 час, и так до финального статуса, а не до истечения терпения. Хаотичных повторов нет, интервалы строго дозированы.
От кассового разрыва страхует ежедневная сверка (Reconciliation). Автоматический сервис скачивает клиринговые реестры банка по SFTP в форматах CSV, MT940, XML. Затем он сопоставляет каждую внешнюю транзакцию по референсному номеру операции (RRN) и коду авторизации. Источник для сравнения: записи в вашем журнале проводок. Расхождение даже в одну копейку автоматически становится задачей дежурному инженеру.
Мониторинг бизнес-метрик
Классическая драма в платёжных сервисах. Grafana светится изумрудно-зелёным светом, утилизация процессоров составляет 12%, оперативная память в норме, системных ошибок 500 нет. А бизнес за последний час потерял миллион рублей оборота.
Причина проста: инфраструктурный мониторинг слеп к бизнес-логике. Контейнеры могут штатно принимать входящие запросы и отвечать 200 OK, а платежи молча падают в очередь ошибок из-за сбоя формата данных. Или отклонять операции на этапе валидации номера карты.
Конверсия оплат (Acceptance Rate) — доля успешных платежей от общего числа попыток. Считается в разрезе платёжных методов (карты, СБП, Mir Pay) и отдельных банков. Падение показателя по конкретному банку сразу указывает на проблемы на стороне его серверов подтверждения (ACS).
Воронка отказов: на каком именно этапе уходят пользователи (ввод реквизитов карты, редирект на 3DS, ввод SMS-пароля).
Сквозная распределённая трассировка — идентификатор trace_id рождается в момент клика в интерфейсе и сопровождает платёж на каждом шаге, от формы карты до реестра банка. Дальше он пробрасывается через все внутренние микросервисы. Банку он передаётся как внешний номер заказа. При расследовании инцидента инженер за секунды видит в системе трассировки полный путь платежа. Со всеми задержками и ответами внешних шлюзов.
Алерты с готовым регламентом действий (Runbook) — алерт не должен просто писать «High Error Rate». Он обязан сообщать: «Конверсия оплат упала ниже 85%. Банк X вернул 50 таймаутов подряд. Инструкция по переключению потока на резервный Банк Y — в регламенте реакции».
Безопасность и комплаенс
Технические команды регулярно приносят такой запрос. «Через две недели у нас аудит безопасности и привлечение инвестиций, помогите быстро закрыть требования регуляторов».
Реальность сурова: безопасность в финтехе невозможно надстроить поверх готового проекта в последний момент.
Сохраните полный номер банковской карты клиента в обычную базу данных без аппаратного шифрования, и поздравляем: вся инфраструктура компании, до последнего стенда, попадает в зону аудита. Вы автоматически попадаете под максимальный уровень международного стандарта PCI DSS. Это влечёт за собой закупку специализированного оборудования, ежеквартальное сканирование уязвимостей и ежегодный дорогостоящий аудит.
Сокращение контура аудита
Главное инженерное правило при работе с платёжными данными звучит просто: лучший способ защитить конфиденциальные данные — вообще к ним не прикасаться.
Первый ход — встраиваемые платёжные поля (Hosted Fields и Web SDK): форма ввода данных карты отображается через защищённые iFrame или SDK, которые держит сторона платёжного провайдера, и данные карты уходят напрямую в сертифицированный контур партнёра, вообще минуя ваши серверы. В ответ система получает лишь обезличенный токен и маску карты вида 411111******1111 (токенизация), с которым можно инициировать регулярные списания подписок: достаточно минимального опросника самооценки SAQ A, и сертифицировать всю инфраструктуру компании не нужно.
Если чувствительные данные всё же придётся обрабатывать — требования 152-ФЗ о персональных данных, положения 683-П/719-П ЦБ РФ, — платёжный сегмент физически и логически отделяется от остальной сети. Доступ к боевым базам строго разграничивается (RBAC), вводятся неизменяемые хранилища логов (WORM: записано один раз, изменить нельзя), а инфраструктура описывается через код (IaC).
Постепенная модернизация
Когда бизнес растёт, старый платёжный монолит неизбежно начинает трещать по швам. Частая реакция менеджмента: «Здесь всё запутано, давайте перепишем платёжный шлюз с нуля за полгода!»
В финтехе попытка переписать ядро с нуля «в один релиз» почти всегда заканчивается провалом. Сроки срываются, а в день переключения всплывают десятки недокументированных нюансов. Из-за них встают реальные платежи клиентов.
Мы используем прагматичный подход: паттерн плавного замещения монолита (Strangler Fig).
Сначала команда изолирует один конкретный сценарий. Это могут быть выплаты на карты самозанятым или механизм частичных возвратов, где риски ограничены понятным периметром. Для этого сценария поднимается новый сервис с чистой архитектурой и строгим контрактом. В нём гарантированная защита от дублей и двойная запись в журнале проводок.
Затем включается теневое дублирование (Shadow Routing). Боевой запрос по-прежнему исполняет монолит, и его ответ сразу уходит клиенту. Одновременно копия запроса асинхронно прилетает в новый сервис. Фоновый верификатор непрерывно сравнивает результаты: сошлись ли итоговые статусы, удержанные комиссии и бухгалтерские проводки.
Когда фоновая сверка подтверждает полную идентичность расчётов, начинается канареечный перевод нагрузки. На новый сервис направляется 1% реального трафика, затем 5%, 25% и постепенно все 100%. На каждом рубеже сверка банковских реестров подтверждает идеальный баланс. Если возникает малейшая аномалия, маршрутизатор мгновенно возвращает поток на старый монолит, без простоя и без ошибок для покупателей. И снаружи никто ничего не заметил.
Устойчивость платёжного контура
Финтех не терпит слепой веры в счастливый сценарий. Надёжную платёжную платформу отличает не модный стек, а хладнокровная готовность к отказам: сеть оборвётся, банк пришлёт неожиданный ответ, а клиент нажмёт кнопку оплаты трижды в вагоне метро. Архитектура обязана спокойно выдерживать такое давление.
Двухэтапное списание и сквозная идемпотентность защищают от повторных списаний, а неизменяемый журнал проводок с двойной записью и ежедневная сверка реестров удерживают целостность балансов. Даже в дни пиковых распродаж. Деньги покупателей остаются в безопасности. Бизнес защищён от кассовых разрывов и претензий финансового регулятора.