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

В этой статье разберем два эпизода из практики: от технических тонкостей до ситуаций на стыке бизнеса и разработки. Поговорим о том, почему слепое доверие документации часто оборачивается болью и как проактивный подход помогает компании сэкономить деньги, а разработчикам — сохранить нервы и сон.

Маргарита Моногарова

Altenar

Меня зовут Маргарита Моногарова, я — тимлид команды интеграций международного разработчика ПО для спортивной аналитики Altenar. Уже восемь лет я занимаюсь бэкенд-разработкой, четыре года руковожу командами. Так уж вышло, что с самого начала карьеры мне стабильно доставались интеграции — и особенно платёжные. За это время я накопила немало «боевых» историй и стала настоящим параноиком в вопросах надёжности и стабильности систем. Сейчас делюсь этим опытом, чтобы другим командам не приходилось наступать на те же грабли.

А для тех, кто ещё ни разу не сталкивался с интеграциями, начнём с простого: что это вообще такое, зачем нужны и почему для них создают отдельные команды.

Что такое интеграция

Ни один бизнес не делает всё сам. И чтобы расти, компании подключают то, что уже создано другими.

Интеграция — это связка между вашим продуктом и внешним сервисом.

Например, нет смысла строить с нуля сложнейшую платёжную систему, если можно подключить готового провайдера. Необязательно самим проверять паспорта и документы клиентов — для этого есть сервисы, типа Know Your Customer (KYC). То же самое с почтовыми шлюзами: вместо своего — готовый API.

Я работаю в компании, которая активно использует сторонние решения и делает высоконагруженные сервисы для букмекерских контор. Сегодня у нас более 600 сотрудников, присутствие в 50+ странах и свыше 10 лет опыта на рынке. За это время накопилось больше 60 интеграций.

Руководителем команды я стала 2,5 года назад. Разумеется, не все интеграции создавались под моим руководством — многие появились задолго до меня. Но я всё равно их знаю. И не потому, что специально изучила каждую с нуля, а потому что до сих пор регулярно сталкиваюсь с проблемами, которые всплывают даже спустя 4-5 лет после релиза.

Когда мне предложили возглавить команду интеграций, я очень обрадовалась. До этого работала в фичевой команде, а «фичи» — это задачи уровня «нужно вчера» или «сделать за два часа». Всё срочно, всё в аврале. В итоге я оказалась в состоянии выгорания. Поэтому новость об интеграциях показалась глотком свежего воздуха: наконец-то можно будет закладывать время на полноценную работу. Ведь все понимают, что интеграция — это не пять минут, не два часа и даже не неделя.

Но радость была преждевременной. С одной-двумя интеграциями справиться ещё можно, но когда их больше шестидесяти — это совсем другая реальность. Рассказать о каждой здесь невозможно. Поэтому я сосредоточусь на платёжных интеграциях — тех, с которыми мы сталкиваемся чаще всего.

Так же чтобы история звучала живее, главным персонажем в моём рассказе будет  разработчик Валера. Он работает в стартапе и делает свою первую платёжную интеграцию.

Эпизод 1. Первая платёжная интеграция Валеры

У Валеры были ожидания. Он думал, что изучит документацию, реализует интеграцию, внесёт правки после QA, выкатит всё на прод и вообще навсегда забудет о задаче.

…но вместо этого он впал в состояние, близкое к депрессии.

Чтобы понять, как так получилось, вернёмся в день, когда интеграция прошла тестирование QA и была доставлена в прод. Валера радостный пошёл домой и лёг спать. Ему снился прекрасный сон о том, как он заработал кучу денег и отдыхает где-то на Мальдивах. Пока не разбудил звонок продакта:

Валера, естественно, в панике. «Как так? Мы же всё проверили! Я всё покрыл тестами и расписал для QA миллион сценариев!» — помчался в офис, открыл логи и увидел:

  • Сообщение об успешном платеже было отправлено провайдером.

  • Сообщение об успешном платеже было принято интеграцией.

Так и должно быть. Но спустя 10 секунд:

  • Сообщение об успешном платеже снова отправлено провайдером.

  • Сообщение об успешном платеже снова принято интеграцией.

Проблема в том, что провайдер при сбое может многократно отправлять одно и то же сообщение. А значит, у интеграции должен быть механизм, умеющий это обрабатывать.

Валера обратился к более опытным коллегам, и те сказали ему всего одно слово — идемпотентность.

Многие знакомы с этим понятием, но для тех, кто не сталкивался, приведу простой пример. Представьте, вы нажимаете на кнопку «Открыть дверь» на брелоке от машины. Нажали один раз — дверь открылась. Нажали второй раз — результат тот же: дверь открыта. Это идемпотентно.

А вот если при первом нажатии одной и той же кнопки дверь открылась, при втором закрылась, а при третьем открылся багажник — это уже не идемпотентно, потому что каждый раз вы получаете разный результат.

Валера оказался умным парнем, он быстренько прочитал об этом и добавил проверку идентификатора платежа и его статуса при каждом сообщении от провайдера. Теперь при повторных запросах приложение корректно отвечало 200 OK, но по факту ничего не делало. То есть ключом идемпотентности в его случае стал именно идентификатор платежа и его статус.

Понимаю, что для многих это очевидно, но эта проблема до сих пор актуальна. Например, недавно мы столкнулись с ситуацией: из-за таймаута первый запрос на вывод средств не вернул ответа и система отправила его повторно. В итоге провайдер обработал оба запроса, несмотря на то, что ключ идемпотентности был передан.

Немножко заглянем внутрь.

Крайне рекомендую не делать ключи идемпотентности на базе уникального индекса в БД. Это звучит красиво, но на практике превращается в боль. Очень легко забыть, что этот уникальный индекс — ключ идемпотентности. И как только вы начинаете расширяться — к примеру, добавляете партиционирование по месяцам (для MySql для этого включаете в уникальный индекс колонку created_at), — вы теряете идемпотентность, даже не заметив.

На самом деле, тема ключей идемпотентности куда сложнее, чем кажется на первый взгляд.

Вернёмся к Валере. На этот раз он пошёл домой чуть менее радостный, но всё же лёг спать. Снился ему снова прекрасный сон — теперь уже про отпуск в Турции. Пока его снова не разбудил звонок продакта:

Валера спросонья вообще ничего не понял: «Как так? Я же только вчера это починил!» Подумал, что, наверное, продакт что-то перепутал. Но как ответственный разработчик он собрался, приехал в офис, открыл логи — и что же он увидел:

  • Сообщение об успешном платеже было отправлено провайдером.

  • Сообщение об успешном платеже было принято интеграцией.

Всё верно. Но в тот же момент, на другой ноде:

  • Сообщение об успешном платеже было отправлено провайдером.

  • Сообщение об успешном платеже было принято интеграцией.

Казалось бы, Валера добавил ключи идемпотентности, но почему-то это не помогло. Дальше разберёмся, как можно было найти источник проблемы.

Ответ кроется в распределённой природе системы. Чтобы разобраться, важно быть знакомым с тремя понятиями:

  • Race condition

Приложения, которые писал Валера, распределённые и работают в нескольких дата-центрах, поэтому одно и то же событие может быть получено одновременно на нескольких нодах. В итоге две операции могут выполняться параллельно.

  • Транзакции

С транзакциями Валера был знаком и даже использовал их в своём решении. Но, как видно, этого оказалось недостаточно. Транзакции сами по себе не спасли ситуацию.

  • Уровни изолированности транзакций

Гораздо глубже лежит тема уровней изолированности транзакций. И вот о ней Валера не знал. Как не знал и то, что Postgres, который он использует, по умолчанию обладает уровнем Read Committed и не блокирует чтение при записи.

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

Есть несколько способов справиться с подобными ситуациями. Например, использовать: 

  • Распределённые блокировки

Самый популярный подход — мьютексы. Это механизм синхронизации, который не даёт нескольким потокам одновременно менять одни и те же данные. Обычно для этого используют внешнюю точку координации, например Redis.

Я всегда вспоминаю аналогию с аквапарком. Представьте, что все посетители хотят скатиться с горки и лезут без очереди. Итог — хаос, кто-то падает в бассейн, кто-то мимо. Мьютекс — это инструктор наверху, который пускает по одному: как только один человек закончил спуск, пускает следующего.

  • Шардирование

Другой вариант — разделение данных на логические части. Например, если бы у нас были шарды по пользователю, то конфликтов просто не возникло бы: разные процессы работали бы со своими сегментами данных.

Конечно, есть и другие подходы — очереди, саги и прочие механизмы. Но сейчас не будем углубляться.

Вернёмся к Валере. Он имплементировал мютексы на Redis, выкатил на прод и пошёл спать со спокойной душой. Ему всё ещё снился хороший сон. Правда, денег заработал чуть меньше, поэтому отдыхал уже в Ялте. И вот снова звонок от продакта:

Валера был, конечно, не рад, но теперь хотя бы проблема другая. Он помчался в офис и открыл логи. Запросов нет. Пришлось лезть в логи к провайдеру, и там всё прояснилось:

  • Сообщение об успешном платеже было отправлено провайдером

  • Сообщение потерялось

  • Приложение об этом не в курсе

Что в этом случае стоит знать, чтобы исправить ситуацию.

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

Схема удобная: приложение получает информацию именно тогда, когда необходимо. Но у этого подхода есть минус — если что-то пошло не так, мы, как принимающая сторона, об этом можем просто не узнать.

В этом случае неплохо было бы дополнить менее популярной схемой, которая есть не у всех провайдеров, но ранее интеграции чаще встречались именно на ней. Это схема на самостоятельном опросе статусов.

Приложение регулярно ходит к провайдеру с вопросом: «А что с моей оплатой? А теперь? А сейчас?». Минус очевиден: мы нагружаем провайдера, а если делаем это редко, данные обновляются с задержкой, словно в замедленной съёмке. Клиентам такой опыт, конечно, не нравится.

Но если объединить обе схемы, получается отличный вариант. Мы используем коллбэки как основной механизм, а если за установленный тайминг (например, 10 минут) от провайдера не приходит никаких изменений статусов, инициируем опрос сами: «А ты не забыл про меня? Может, что-то изменилось?». Такой подход выглядит лучше.

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

Но домой снова пошёл тревожным. Во сне ему мерещилось, что денег он так и не заработал и в отпуск его никто не отпустит. И снова звонок продакта:

Валера уже на нервах. Быстро оделся, помчался в офис, открыл логи. Что же пошло не так?

  • Сообщение о платеже в процессе обработки было отправлено провайдером. 

  • Сообщение об успешном платеже было отправлено провайдером. 

Пока на стороне провайдера всё выглядит корректно. Но вот что на стороне приложения:

  • Сообщение об успешном платеже было получено приложением.

  • Сообщение о платеже в процессе обработки было получено приложением.

Проблема в том, что статусная модель Валеры нарушилась. Он даже не думал, что такое возможно, но оказалось, что провайдер не гарантирует последовательность статусов. Их нужно отслеживать самостоятельно. Для многих платёжных интеграций простой и рабочий вариант — использовать механизм допустимых переходов. Когда есть чёткая схема, статусы идут только вперёд, и никаких проблем не возникает.

Но у подхода есть ограничения. Если менять статусы «вбок» (например: сначала платёж active, потом suspended, потом снова active), допустимые переходы уже не спасают. Нужно договариваться с провайдером и уточнять, как различать запросы: какой первый, какой второй. Но, к сожалению, универсального решения нет.

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

Валера, конечно, «в восторге» от происходящего, но встал и поехал на работу. Как так получилось? 

Валера оказался слишком ответственным. В документации провайдера он прочитал: срок жизни ссылки на оплату — 15 минут. Поэтому все платежи в статусе open, которым было больше 20 минут, он автоматически отменял. Логично, казалось бы — ссылка на оплату уже не актуальна, но только через 10 минут после отмены пришло:

  • Сообщение об успешном платеже было отправлено провайдером

  • Сообщение об успешном платеже НЕ было принято приложением

Помните, мы реализовали корректную схему обработки статусов? Здесь она подвела. А Валера понял, что перестарался. Исправить оказалось просто: он удалил свою реализацию с отменой платежа. Платежи оставались в открытом статусе, и проблема была решена.

Валера за время этой истории прошёл почти все стадии принятия.

Отрицание. Он не мог поверить, что с его интеграцией что-то не так. Он ведь очень старался!

Гнев. Злился на провайдера: «Почему он не указал эту информацию? Почему я должен сам до этого додумываться?»

Торг. Уговаривал себя: «Сегодня я баг исправлю — и точно посплю нормально. Больше меня никто не разбудит».

Депрессия. В ней он и оказался. Но, чтобы выйти дальше к принятию, Валере нужно было осознать пару вещей.

Стоит ли верить документации? Казалось бы, конечно, да: это же протокол. Но правильный ответ — нет. Документации слепо доверять нельзя. Даже если поля описаны, они могут оказаться неактуальными, иметь другие типы или просто не соответствовать реальности. Провайдеры не описывают массу ситуаций, с которыми придётся столкнуться. Поэтому важно советоваться: с коллегами, сообществом, кем угодно, лишь бы получить опыт из первых рук.

Стоит ли верить в стабильность провайдера? Хотелось бы, но и здесь правильный ответ — нет. Каким бы надёжным и крупным ни был провайдер, всё, что может пойти не так, рано или поздно пойдёт не так.

В итоге Валера дошёл до состояния «не верю никому». Что-то переписал сам, начал хранить логи в персистентном хранилище — и это оказалось правильным решением. Со временем пришло и принятие: он понял, что неплох как разработчик. Просто никто не подсказал, что такие ситуации вообще возможны.

Если эту статью читают представители платёжных провайдеров, у меня есть к вам есть просьба. Попробуйте поставить себя на место разработчика, у которого нет команды и наставника. Подумайте, что стоит добавить в документацию, чтобы помочь такому человеку не наломать дров. Никто не ждёт, что вы опишете всё пошагово, но хотя бы пару базовых понятий точно не повредят.

Что ж, с первым эпизодом мы закончили, переходим ко второму.

Эпизод 2. Первая переписанная платёжная интеграция Валеры

Чёрный лебедь — это событие, которое изначально кажется редким и труднопрогнозируемым, но постфактум часто оказывается вполне логичным, исходя из сложившейся ситуации.

Нассим Талеб, известный математик и экономист

Чёрные лебеди прятались до 1697 года, человечество вообще не знало об их существовании, а теперь они кажутся обыденностью. 

Прошло полгода. Валера жил спокойно… пока бизнес не решил выйти на рынок Японии. Сначала Валера был в шоке, но пошёл смотреть документацию провайдера. Там всё выглядело просто: поддержка разных валют, возможность запросить курс. Но стоило копнуть глубже, как оказалось, что у японской иены нет копеек или центов!

Валера уже начал разбираться, как это решить, когда его отвлекла бухгалтерия. Бухгалтер спрашивал, почему отчёт в системе не совпадает с отчётом провайдера. Валера даже представить не мог, что пошло не так, но не успел начать разгребать проблему, когда его перебил продакт: клиенты нажимают кнопку «Вывести 10 000 рублей», а деньги списываются дважды, трижды и так далее. И таких случаев — не один. Валера понял: из компании утекли деньги. Пришлось срочно переключаться на этот баг, но снова прибежал продакт с криками: «Провайдер прекратил обслуживание на неопределённый срок! Резервного канала оплаты нет!»

Если вам кажется, что такой сценарий маловероятен, напомню пару историй. В 2010 году из-за DDoS-атаки на сервера ПС «Ассист» бронирование авиабилетов на сайте Аэрофлота не работало целую неделю. Но таких проблем хватает и сегодня. Например, 2023 год, система «Золотая Корона», плюс-минус та же ситуация.

Рефлексия

В итоге: провайдер не работает, баги чинить не надо, потому что чинить-то уже нечего. Поэтому Валера сел рефлексировать: что пошло не так и что можно было сделать иначе и начал разматывать весь комок проблем с самого конца.

Чужая зона ответственности

Провайдер обещал красивый SLA в 99,9%, но Валера понимал: такие цифры в реальности редко выполняются. А если что-то сломается, отвечать всё равно придётся ему. Он даже не подумал обсудить этот риск с бизнесом. Возможно, бизнес бы не согласился, но хотя бы тогда у Валеры была бы моральная подстраховка: «Я же предупреждал». А без этого остаётся только молча разбирать последствия.

«Подумаем об этом завтра»

Свою первую интеграцию Валера делал только под одного провайдера. Мысль о мультипровайдерной схеме он отложил «на потом». В итоге, когда бизнесу понадобился ещё один провайдер, внедрение растянулось надолго: слишком много общей логики оказалось жёстко завязано на старый код.

Отсутствие подстраховки

Валера вспомнил и о баге с многократными списаниями. Здесь можно было заложить защиту с самого начала. Никто ведь не запрещал ввести базовые ограничения:

  • дневные лимиты,

  • верификацию при больших суммах,

  • алерты на аномальные пики транзакций.

Да, часть мер нужно согласовывать с бизнесом, но многие из них можно внедрить и без долгих обсуждений. Если бы такие антифрод-правила были реализованы с самого начала, Валера сам бы заметил дорогую ошибку и исправил её до того, как прибежал продакт. Но, к сожалению, этого не произошло.

Прорефлексировав над проблемой утечки денег из-за позднего обнаружения бага, Валера вернулся к вопросу от бухгалтерии и иенам.

Тут его ждал сюрпиз, оказывается, он накосячил в при проектировании баз данных. Думал: дело простое, ничего сложного, и не заморачивался. Вы, наверное, знаете это состояние, когда уставший сидишь над миграцией для новой таблицы и думаешь: «Да нормально всё, справлюсь». А потом оказывается, что выбранный тип данных не подходит.

Вот и Валера не задумался, не прогуглил, не посоветовался с коллегами, а оказалось, что использовать float для работы с деньгами — плохая идея.

Напомню: программирование — это не всегда математика. Дело не в языке программирования, Валера использовал вполне нормальный. А в том, как компьютер работает с данными. Использовать типы с десятичной арифметикой, где числа произвольной длины хранятся без потери точности, было бы гораздо лучшим решением. Либо хранить суммы в минимальных единицах (например, в центах). Тогда бы решился и вопрос с бухгалтерией, и с японскими валютами без копеек.

Вместо этого из-за float возникли ошибки округления:

0.1 + 0.2 = 0.30000000000000004

В итоге мы получили набор проблем:

  • нет поддержки мультипровайдеров,

  • нет лимитов,

  • везде используется float,

  • не предусмотрена работа с валютами без копеек.

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

Советы от Валеры

Другим разработчикам, особенно в стартапах, чтобы высыпаться

Документации нельзя слепо верить

Документация — это протокол, но вовсе не факт, что он будет соблюдён. Лучше лишний раз посоветуйтесь с коллегами или спросите у сообщества, что может пойти не так.

Не рассчитывайте на стабильность провайдера

Мы часто верим, что провайдер будет работать всегда. Но на его стороне может очень многое пойти не так. Поменяются справочники, и если вас не предупредили — узнаете об этом только когда систему завалят алерты. Провайдер может банально «вырубиться». Думайте о том, что может сломаться, и старайтесь предусмотреть это заранее.

Больше логов

С платёжными провайдерами логи — это святая святых. Разбираться придётся много, дотошно, а иногда и с вопросами материальной ответственности. Пример: деньги списались дважды, потому что не было ключа идемпотентности. Чтобы в таких ситуациях выяснить, кто виноват, нужно разбираться с провайдерами, имея детальные логи.

Проектируйте «на берегу»

Совет спорный, потому что никто не любит овер-инжиниринг. Но если бизнес уже начал работать с интеграцией, велика вероятность, что на ней всё не закончится: появятся новые страны, новые города, новые провайдеры. Лучше это сразу закладывать и хотя бы какие-то моменты общей логики вынести отдельно.

Будьте проактивны

Формально зона ответственности разработчика ограничена. Но именно нам потом вставать по ночам и фиксить. Поэтому иногда лучше предупредить бизнес: сказать, что SLA в 99,9% звучит красиво, но в реальности всё может быть иначе. Возможно, вас не послушают. Но так вы хотя бы сохраните себе нервы и добавите несколько часов сна.

Если у вас возникли вопросы по теме материалов, их можно задать в личных сообщениях в Telegram.

Скрытый текст

А если вы хотите больше узнать о работе с высоконагруженными системами, пообщаться с ведущими специалистами IT-сферы и обменяться опытом с коллегами, не пропустите конференцию HighLoad++ 2025! Мероприятие пройдет 6-7 ноября в Москве. Принять участие можно как онлайн, так и оффлайн.

Комментарии (6)


  1. sshmakov
    29.10.2025 09:54

    И как только вы начинаете расширяться — к примеру, добавляете партиционирование по месяцам (для MySql для этого включаете в уникальный индекс колонку created_at), — вы теряете идемпотентность, даже не заметив.

    Не стоит путать уникальный индекс, первичный ключ и ключ партиционирования

    Но в целом подборка кейсов и их решений достойная, спасибо


  1. sshmakov
    29.10.2025 09:54

    Гораздо глубже лежит тема уровней изолированности транзакций. И вот о ней Валера не знал. Как не знал и то, что Postgres, который он использует, по умолчанию обладает уровнем Read Committed и не блокирует чтение при записи.

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

    Для этого сначала запись блокируется оператором SELECT FOR UPDATE SKIP LOCKED или NOWAIT, а уж потом все остальное.


    1. dph
      29.10.2025 09:54

      А зачем SKIP LOCKED? Тут же как раз нужна блокировка.
      SKIP LOCKED сделан для очередей, а не для попыток установить лок.


      1. sshmakov
        29.10.2025 09:54

        Конструкция FOR UPDATE ставит лок, а SKIP LOCKED нужен для того, чтобы не тратить время на те записи, где лок уже стоит


  1. svetayet
    29.10.2025 09:54

    отличная статья. прям спасибо!


  1. dph
    29.10.2025 09:54

    А почему задачу по платежной интеграции поручили джуну без опыта? И при этом его решение даже миддл не посмотрел?
    И почему вообще нет стандартного шаблона для платежных интеграций? Обычно он появляется после третьей попытки...