Лаконичный мультиплатформенный язык, разработанный JetBrains
Kotlin KotlinConf
Обзор KotlinConf: уроки от архитектора Verifone об использовании Kotlin в критически важных платежных системах
Четырехлетний сын Юрия Геронимуса однажды спросил его, кем он работает. Юрий ответил, что пишет книги: книги, которые описывают то, что он хочет изменить в физическом мире. Сын не до конца это понял.
Впрочем, как и большинство инженеров: если вы понимаете слова в коде вашего приложения, вы строите хорошие системы. Если нет — вы создаете баги.
В этом суть доклада Юрия Геронимуса на конференции KotlinConf 2026 «Kotlin как защитная сетка: типизированная надежность в финансовой индустрии». Юрий — архитектор в компании Verifone, где он работает над платежными терминалами, шлюзами и системами, соединяющими их с банками и регуляторами в регионе EMEA. Его метод в двух словах звучит так: пишите код на точных терминах спецификаций регулятора или платежной сети и заставляйте компилятор следить за их соблюдением. Обеспечивайте надежность не за счет дисциплины, а с помощью компилятора.
Посмотрите доклад с конференции KotlinConf 2026
Посмотрите доклад Юрия Геронимуса на KotlinConf 2026, чтобы узнать, как Kotlin помогает создавать надежные платежные системы.
Мир за пределами терминала
Прежде чем перейти к паттернам, Юрий обозначил ставки. «Платеж» — это не одно приложение. Это мобильное приложение для продаж, POS-приложение, платежный терминал на базе защищенной ОС, платежные шлюзы, системы управления терминалами, мерчант-платформы и, в основе всего этого, процессинговые центры, банки и регуляторы. В каждой стране действуют тысячи страниц спецификаций и примерно 1000 сценариев, которые должны быть выполнены до того, как что-то попадет в продакшен.
Некоторые из этих терминалов по закону должны работать в автономном режиме до 40 дней, поскольку в стране их эксплуатации платежный терминал является критической инфраструктурой того же уровня, что и заправка или аптека. Командам платежных систем также приходится удовлетворять противоречащие друг другу правила разных регуляторов, управляющих одной и той же общей инфраструктурой: в одной стране требуется определенное поведение, в другой оно запрещено, и оба требования должны быть реализованы в одной кодовой базе. Именно в таких условиях должна выдерживать система типов. Не на демостенде. В продакшене, по всему миру, на протяжении десятилетий, а не одного года.
Шесть способов, которыми одно слово может сломать платежную систему
Юрий разобрал реальные паттерны инцидентов из своего 15-летнего опыта работы в сфере платежей и банков, каждый из которых сводится к одному пропущенному или неправильно использованному слову:
- Некорректное поле привело к отмене операции, и чужие деньги ушли не на тот счет.
- Обязательное поле было сделано допускающим значение null «на всякий случай». Когда обновление прошивки перестало его отправлять, код перешел на значение по умолчанию, и часть парка терминалов перестала обрабатывать платежи.
- Номер карты попал в строку лога и стек вызовов: это прямое нарушение обязательств по неразглашению и реальная проблема с комплаенсом.
- Транзакция была возвращена после того, как ее уже отменили, и с мерчанта списали деньги дважды.
- Авторизация прошла успешно как раз в момент срабатывания тайм-аута, и деньги ушли в результате кражи.
Каждый из этих случаев сводится к одной первопричине: где-то код разошелся во мнениях со спецификацией относительно того, что означает то или иное слово, и ничто не выявило это несоответствие до выхода в продакшен.
Метод: напишите спецификацию, а затем позвольте компилятору проверить вашу работу
Юрий описал это как набор паттернов, а не как фреймворк. Десять паттернов, выполняющих одну и ту же задачу: сделать невозможным создание некорректных состояний, а не просто обрабатывать неверные данные позже.
1. Классы значений для существительных из спецификации
Модели мошенничества и рисков содержат десятки похожих по структуре идентификаторов: ID мерчанта, ID терминала, ID эквайера и другие, причем некоторые из них буквально идентичны по формату из-за требований регуляторов. Если это обычные строки, ничто не помешает рефакторингу или копипасту случайно перепутать два из них, и транзакция молча уйдет не тому эквайеру, например. Решение Юрия — использовать отдельный класс значений (value class) для каждого идентификатора.
Поменяйте аргументы местами сейчас, и код просто не скомпилируется. Ошибка, которая раньше доходила до продакшена, превращается в ошибку сборки прямо на машине инженера.
2. Фабрики валидации
Любое внешнее поле, будь то данные из API банка или прошивки терминала, поступает в виде нетипизированной строки, которая может означать совсем не то, что предполагает спецификация. Паттерн Юрия заключается в том, чтобы немедленно парсить такие данные на границе в sealed-результат, вместо того чтобы доверять им внутри системы.
Юрий считает этот подход очень хорошей практикой: он не генерирует исключения, помогает избежать наихудших вариантов с использованием Result<T>, который скрывает бизнес-домен, а также предотвращает подводные камни runCatching в корутинах.
3. Необнуляемые типы на границе
Часто обязательные поля помечаются как допускающие значение null «на всякий случай», что требует жонглирования скрытыми фоллбэками, которые часто маскируют истинного виновника. Юрий предлагает задействовать механизмы безопасности Kotlin для работы с null, чтобы сообщать об ошибке громко и в одном месте:
Это решение проблемы сбоя парка терминалов: поле, сделанное обнуляемым «на всякий случай», больше никогда не получало молчаливое значение по умолчанию. Оно дает сбой один раз, громко, в единственном предсказуемом месте парсинга, где его можно отслеживать, вместо того чтобы тихо падать где-то в глубине системы, где за ним никто не наблюдает.
4. Маскирование конфиденциальных данных по умолчанию
Номера карт передаются через логи и стектрейсы в виде обычных строк, и это потенциальное нарушение требований конфиденциальности. Юрий оборачивает значение в класс с приватным конструктором и переопределяет метод toString(), благодаря чему безопасное поведение становится стандартным поведением.
Случайный вызов log.info(“processing $pan”) теперь выводит четыре замаскированные цифры. Получение реального значения — того единственного, что действительно необходимо для связи с платежной сетью, — требует явного и проверяемого вызова. В этом пути больше нет ничего невидимого. Небольшое примечание от Юрия: в реальных системах все было гораздо сложнее.
5. Каждому состоянию — свой класс, а не просто свое имя
Транзакция, по которой уже был выполнен возврат, никогда не должна иметь возможности вызвать метод refund снова. Однако единый изменяемый класс Transaction с полем status полагается на то, что кто-то не забудет проверить этот статус везде. Продвинутая версия от Юрия наделяет каждое состояние собственным дата-классом, поэтому метод не может изменить состояние транзакции из неподходящего дата-класса. Это гарантирует, что переход, которого быть не должно (например, возврат уже отмененной транзакции), будет заблокирован компилятором.
6. Неизменяемость и именованные переходы
Это продолжение предыдущего паттерна. Сделайте класс с вашими бизнес-данными неизменяемым и дайте каждому переходу имя. Имя — это глагол из спецификации. Любой метод, который не должен быть вызываемым, просто физически не существует:
Юрий назвал это своим любимым паттерном, потому что он влияет на то, как инженеры читают спецификацию. Это меняет то, как инженеры думают о бизнес-домене и говорят о нем.
7. Удалите ветку else
Если в спецификации явно перечислены все случаи и никогда не говорится «в противном случае», то оператор else в коде означает изобретение ситуации, которую спецификация не описывает. Его команда имела дело только с полными возвратами. Когда частичные возвраты стали реальным сценарием, старая ветка else поглотила бы их молча: код компилируется, тесты проходят, но в продакшене это работает неверно. Удалите else, и тот же самый новый случай превратится в ошибку компиляции, которая укажет на каждое место, требующее обновления.
Платежи в одной стране постоянно отклоняются то ежеквартально, то ежемесячно, а в масштабах всего региона EMEA — это происходит ежедневно в случае Юрия. Исчерпывающий вариант превращает каждое отклонение в список того, что именно должно измениться, а не в баг-репорт от клиента, с которого списали не ту сумму.
8. Выберите область видимости сопрограммы, которую подразумевает спецификация
Пример Юрия с Черной пятницей: авторизация, управление рисками и проверки на мошенничество раньше выполнялись в виде трех независимых вызовов, каждый в своем блоке try/catch. Если проверка на мошенничество выполнялась медленно под нагрузкой, транзакция все равно могла быть одобрена, в то время как проверка на мошенничество молча завершалась с ошибкой, и украденные карты начинали одобряться. Kotlin предоставляет две области структурированной конкерентности, которые выглядят похоже, но ведут себя совершенно по-разному, и именно спецификация, а не удобство, должна определять, какую из них вы используете.
Здесь coroutineScope завершается сбоем быстро: если проверка на мошенничество не удается, авторизация и риски также отменяются, и ничто не одобряется молча. supervisorScope позволил бы дочерним элементам продолжать работу независимо, что является правильным решением для таких задач, как рассылка уведомлений методом веера, где один сбой не должен отменять остальные. Вопрос не в том, какая область видимости более идиоматична. Вопрос в том, что говорит спецификация о том, что должно происходить при сбое одной из ветвей.
9. Моделируйте тайм-аут как доменный результат, а не как исключение
Вызов шлюза, у которого истек тайм-аут, не обязательно является сбоем. Эквайер мог уже обработать его; клиент просто не получил ответ вовремя. Перехват этого события как общего исключения и переход дальше стирает разницу между «ничего не произошло» и «что-то произошло, но мы просто не уверены, что именно». Исправление Юрия моделирует таймаут (Timeout) как результат первого класса в домене.
TimedOut — это не отсутствие ответа. Это отдельное состояние, которое направляется в запрос на сверку, а не на повторную попытку, потому что наивная повторная попытка после тайм-аута, который на самом деле успешно прошел выше по потоку, — это именно то, из-за чего с клиента в итоге списывают деньги дважды.
10. Сделайте тесты такими, чтобы их мог прочитать регулятор
Самый неожиданный паттерн Юрия нацелен на сертификационные тесты, а не на код приложения. Типичный набор сертификационных тестов состоит из тысячи тестов примерно по 200 строк каждый, и, по его словам, это не под силу прочитать даже архитектору. Команда переписала тесты на Kotlin DSL. Написанный таким образом тест может прочитать специалист по сертификации, никогда не писавший на Kotlin, проследить логику в соответствии со спецификацией и подписать ее напрямую. Тест перестает быть артефактом, который могут проверить только инженеры по тестированию, и становится спецификацией, которую могут просмотреть бизнес и специалисты по сертификации.
Что на самом деле изменилось
Юрий приводит конкретные результаты: разработка ускорилась в 2,7 раза, а сертификация была пройдена всего с одним дефектом и с первой попытки — настолько необычно, что регуляторы спросили, что изменилось. Получившаяся кодовая база оказалась примерно на 30% меньше. Ревью кода сместилось с поиска пропущенных проверок состояния на сравнение кода со спецификацией предложение за предложением, поскольку компилятор теперь отлавливает тот класс ошибок, которые рецензенты раньше искали вручную. Ввод в работу нового инженера раньше занимал две-три недели, прежде чем он понимал достаточно из 5000-страничной спецификации и унаследованной кодовой базы, чтобы отправить хоть что-то в продакшен. После этого новые сотрудники открывали пулреквест в продакшен уже через несколько дней. Инженеры также могли переходить между командами устройств и серверными командами, чему раньше мешала разделенная кодовая база на двух разных языках. Последний эффект, который отметил Юрий, заключается в том, что инженеры начали говорить на языке бизнеса достаточно хорошо, чтобы напрямую общаться с продакт-менеджерами, отделами продаж и регуляторами, порой предлагая изменения в сами правила.
Модернизация платежей без переписывания
Юрий прямо говорит об ошибке, которую совершает большинство команд: отношение к миграции как к единому проекту, переписыванию на Kotlin. Его системы используют 25-летний код на Java и C++, обрабатывающий живые транзакции каждую секунду, без допустимого времени простоя, поэтому переписывание никогда не стояло на повестке дня. Если рассматривать это как выбор между рефакторингом и переписыванием, то для работающей платежной платформы ответ почти всегда — рефакторинг. Его альтернатива — четыре параллельных, а не последовательных направления.
- Сначала оптимизируйте существующую кодовую базу на Java или C++, не трогая язык; модернизируйте структуру кода прямо сейчас — общая компоновка, чистая архитектура, кричащая архитектура — еще до того, как появится какой-либо код на Kotlin.
- Перейдите на разработку через приемочные тесты, а не через модульные. Никого за пределами инженерной команды не волнует, что находится внутри функции; всех волнует поведение от конца до конца.
- Создайте функции пригодности для проверки соответствия требованиям, безопасности, масштабируемости и других нефункциональных требований, которые работают параллельно с миграцией, а не оцениваются в самом конце.
- Мигрируйте постепенно: сначала новые адаптеры, затем новые доменные модели, а затем устаревающий код, который меняется чаще всего, добиваясь того, чтобы количество «общих портов» сократилось до нуля. Именно совместимость Kotlin с Java делает этот инкрементальный путь вообще возможным: новый адаптер может соседствовать с неизмененным кодом на Java без периода двойного обслуживания.
Запуск всех четырех процессов одновременно, по словам Юрия, делает усилия по модернизации платежей безопасными и рентабельными в регулируемой среде. Их последовательный запуск приводит к застою миграции.
Миграция — это инвестиционное решение, а не языковое предпочтение
Юрий указал, кого нужно убеждать в масштабах предприятия: это не инженерная команда. Он сослался на выступление команды Uber накануне его выступления на том же мероприятии, где высказывалась похожая мысль: даже компания, известная своим быстрым развитием, относится к миграции языка как к инвестиционному решению, когда работает в масштабах предприятия. Достоверное экономическое обоснование должно учитывать затраты на параллельную работу старых и новых систем, затраты на вывод из эксплуатации заменяемых компонентов, кривую обучения для команды и риск самого перехода. Получение одобрения на миграцию не равносильно ее принятию. Без операционной модели того, как миграция фактически работает изо дня в день, Юрий был резок: в конце ее не ждет счастливый конец.
Он указал на ресурс для устранения именно этого пробела: 22-page playbook on the Kotlin site, опубликованный за два дня до этого выступления и охватывающий, по его словам, «ту часть, которую все пропускают».
Каков основной метод Юрия Геронимуса в одном предложении?
Пишите код буквальными словами спецификации и заставляйте компилятор обеспечивать их соблюдение, вместо того чтобы полагаться на дисциплину или документацию для устранения пробелов между ними.
Обязательно ли переписывать устаревшую систему на Java или C++, чтобы использовать эти паттерны?
Нет. Юрий прямо выступает против того, чтобы рассматривать миграцию как проект по переписыванию. Его четыре направления выполняются параллельно: оптимизация существующей кодовой базы, переход к приемочному тестированию, уделение большего внимания функциям пригодности и инкрементальная миграция, начиная с адаптеров и доменных моделей, а затем переходя к наиболее часто изменяемым частям.
Почему дисциплина или ревью кода не позволяют выявить эти ошибки?
При том масштабе, с которым работает Юрий — 20 команд по всему миру, — дисциплина не масштабируется равномерно. Проверка компилятором применяется одинаково везде и всегда, независимо от того, кто находится на дежурстве или насколько внимателен тот или иной рецензент в этот день.


![10 рисков безопасности при vibe coding, которые нельзя игнорировать [+ Лучшие практики]](https://softteco.com/wp-content/uploads/2026/10/Vibe-coding-security-min.png)





