Продолжение статьи о продуктовой гипотезе и дистрибуции. Здесь — только инженерная часть.

Введение

В первой статье я описывал продуктовую гипотезу: возможно ли построить B2B SaaS-платформу и со временем маркетплейс приложений, не забирая у вендоров код, инфраструктуру и контроль над backend-сервисами.

Эта статья — продолжение, но уже с инженерной стороны. Она не про рынок и не про стратегию, а про то, как такая платформа реально проектировалась и строилась, какие технические решения мы принимали и почему итоговая архитектура выглядит именно так.

Откуда вообще появилась идея платформы

Идея платформы родилась не из архитектурных экспериментов и не из желания «сделать экосистему».

Внутри группы компаний существовало много разных продуктов и еще к тому же, нам на поддержку постоянное передали в контур новые и новые. Они решали разные бизнес-задачи, но почти у каждого был собственный back-office. Когда мы вместе с архитектором и CTO посмотрели на эти решения внимательнее, стало очевидно: при всей разнице предметных областей они снова и снова реализуют один и тот же набор функций.

Пользователи и роли, аудит действий, статистика, отчетность, подключенные финансовые решения, административный интерфейс и не только… — всё это каждый раз писалось заново. Эти части редко являлись конкурентным преимуществом, но стабильно съедали время и ресурсы.

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

Почему мы начали с требований, а не с архитектуры

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

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

Ключевой момент: требования группировались не по сервисам и не по технологиям, а по продуктам как бизнес-сущностям.

В нашей модели продукт — это не один backend-сервис. Один продукт может включать несколько сервисов, а один сервис может использоваться в нескольких продуктах.

Только после этого к работе подключился наш архитектор.

Архитектура сверху вниз

Первым человеком, который начал помогать выстраивать общую архитектурную картину, стал наш архитектор — Максим Шлемарев.

Максим Шлемарев

Solution Architect / Архитектор

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

Этот подход можно описать как проектирование сверху вниз. Сначала формировалась универсальная модель платформы без привязки к реализации. Затем, при переходе к конкретному продукту, архитектура детализировалась через бизнес- и системную аналитику.

Верхнеуровневая схема всей системы
Верхнеуровневая схема всей системы
Подробнее по схеме
  • Где Api-client - возможность обращаться в систему по публичному апи;

  • [react] Backoffice - фронтент-приложение написанное на React;

  • Единственная публичная точка обращения к backend системы - это ApiGateway;

  • Все остальные backend-сервисы закрыты от внешнего мира в kubernates;

  • В роли Edentity auth manager’а используется коробочное решение Keycloack;

  • Генерация и проверка прав управляется отдельным user-access-приложением написанным на golang;

  • СоmmonServices - основные core-сервисы управления системой (организации, приложения, продукты, конструктор страниц);

  • Billing - фин.част системы (платежный шлюз, управление заказами, платежные системы, терминал/касса);

  • В качестве отслеживания за состоянием системы (ошибки, логирование, трассировка) используется: sentry, grafana, prometeus, elk/kibana, jaeger.

Каждый сервис проектировался так, чтобы быть максимально атомарным.

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

Для того чтобы система была максимально безопасна и фильтровала в себе весь входящий трафик, была выбрана одна публичная точка входа (ApiGateway). Все остальные сервисы изолированы внутри kubernates. 

ApiGateway в зависимости от входящего запроса знает к какому сервису обращаться, агрегировать запросы к нескольким сервисам либо вовсе блокировать запрос. Любой действие в системе логируется. 

Чтобы в каждом из бизнесовых сервисов не приходилось делать свою ролевую модель и управления правами был реализован отдельно стоящий сервис с общим RBAC - контрактом. В одном месте происходит авторизация пользователя, задаются права. Вертикальные права контролируются на уровне единой точки входа (ApiGateway). Таким образом ролевую модель можно настраивать для любого сервиса, в том числе и вендора.  Плюс имеется возможность при передаче данных общего контракта контролировать горизонтальные права на уровне каждого из сервисов при необходимости.

Как от архитектуры перешли к UI и UX

Довольно быстро стало ясно, что основная сложность всех этих продуктов не только архитектура.

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

Интерактивный прототип в Figma
Интерактивный прототип в Figma
Админка на React собранная через генератор

Уровень детализации был близок к будущей реализации, что позволяло обсуждать с бизнесом не требования, а реальные действия пользователей в UI. Это стало отличным решением, чтобы снять большую часть неопределенности и объяснить бизнесу как возможно будет эксплуатировать спроектированные продукты.

В Figma спроектированы больше сотни страниц
В Figma спроектированы больше сотни страниц
+ все связаны как единый прототип
+ все связаны как единый прототип

Для создания интерфейсов мы осознанно выбрали IBM Carbon Design System — не как визуальный стиль, а как инженерно описанную систему проектирования утилитарных B2B-интерфейсов. Команда IBM проделала огромную работу, описав frontend-компоненты и UX-паттерны для back-office решений. На этих принципах построена и крупнейшая SaaS-платформа IBM — IBM Cloud.

Как из прототипов UI вырос конструктор страниц

На этапе прототипирования стало очевидно, что административный UI состоит из ограниченного набора повторяющихся паттернов. Таблицы с действиями на сущностью, карточки сущности и ее редактирования и карточки ее создания — стандартные действия CRUD (Create, Read, Update, Delete), всё это повторялось от продукта к продукту, от раздела к разделу.

В этот момент стало ясно, что нет смысла программировать каждую страницу отдельно. Так появился конструктор страниц — отдельный инструмент внутри платформы. Страницы описываются декларативно через конфигурацию, а платформа сама рендерит готовый back-office UI.

Страницы описываются декларативно через JSON-конфигурации. Платформа рендерит back-office UI самостоятельно, проверяя права доступа и обращаясь к удалённому backend-сервису вендора за данными.

Флоу выглядит так:
  1. В сервисе product-settings регистрируется продукт;

  2. Описываются маршруты и страницы через соответствующие разделы UI;

  3. Импортируются JSON-конфиги страниц;

  4. В user-access настраиваются права на страницы и компоненты;

  5. При открытии страницы конструктор:

    1. Проверяет права;

    2. Запрашивает данные у backend-сервиса;

    3. Рендерит UI.

Неправильно сформированный конфиг не ломает систему — он просто не применяется.

Пример реального Json конфига страницы раздела: Продукты
{
  "titleKey" : "UI.Foundation.Products.title",
  "entityIdName" : "productId",
  "templateType" : "table",
  "content" : {
    "model" : "products",
    "action" : "index",
    "columns" : [
      {
        "id" : "name",
        "type" : "text",
        "labelKey" : "UI.Foundation.Products.tableHeaderProduct",
        "isSortable" : true,
        "pageKey" : "foundationProductEdit"
      },
      {
        "id" : "vendorName",
        "type" : "text",
        "labelKey" : "UI.Foundation.Products.tableHeaderVendor",
        "isSortable" : true
      },
      {
        "id" : "description",
        "type" : "text",
        "labelKey" : "UI.Foundation.Products.tableHeaderDescription",
        "isSortable" : false
      }
    ],
    "columnsActions" : [
      {
        "id" : "edit",
        "labelKey" : "UI..Table Fast Edit.title",
        "pageKey" : "foundationProductEdit"
      }
    ],
    "toolbarOptions" : {
      "actions" : [
        {
          "labelKey" : "Table Toolbar.buttonCreate",
          "type" : "primary",
          "pageKey" : "foundationProductCreate",
          "icon" : "Add"
        }
      ],
      "filters" : {
        "search" : true,
        "refreshTable" : true,
        "queryFilter" : [
          {
            "queryKey" : "organizationId",
            "labelKey" : "UI.Foundation.Products.tableFilterVendorLabel",
            "type" : "filterableMultiselect",
            "options" : {
              "product" : "Foundation",
              "model" : "organizations",
              "action" : "listByVendor"
            }
          }
        ],
        "settings" : true
      }
    },
    "batchActions" : [],
    "pagination" : true
  }
}

Микросервисы, инфраструктура, финансы и SAGA

Платформа построена на микросервисной архитектуре и развернута в AWS и Google Cloud. Она Kubernetes-native и масштабируется на уровне сервисов.

Для контроля работы такой большой системы и по��держания ее работоспособности 24/7 было релизовано автоматическое поднятие подов в kubernates, набор всевозможных технических показателей в prometeus, которые алертят в случае каких-то неисправностях.

Также контролируются и бизнесовые метрики.

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

Главный коммерческий финансовый продукт требовал особого внимания

Далее хотим поделиться самой полезной и интересной частью - SAGA, чтобы не затягивать и без того длинную статью. Архитектор Максим Шлемарев

При синхронном взаимодействии сервис Accounts мог не получить ответ от PaymentAggregator из-за таймаута или сетевого сбоя, даже если платёж был успешно обработан. Это приводило к несоответствию данных.

Чтобы сервис Accounts всегда знал реальный статус операции — даже при сбоях, уходе клиента со страницы или остановке одного из сервисов — был реализован паттерн SAGA.

SAGA
SAGA
Бизнесовая схема описания платежного шлюза
Бизнесовая схема описания платежного шлюза

Ранее при синхронном взаимодействии сервис Accounts отправлял запрос в сервис PaymentAgregator и не получал гарантированного ответа. Если в PaymentAgregator платеж был обработан, но сервис Accounts не «дождался» ответа по причинам таймаута / ошибки сети, созданная и успешно проведенная транзакция в сервисе PaymentAgregator оставалась в сервисе Accounts в статусе «IN_PROGRESS», по причине чего возникала такая проблема, как несоответствие данных.

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

  • Сервис PaymentAgregator приостановил работу по причине технических неполадок;

  • Клиент покинул страницу оплаты;

  • Произошел сбой в сети.

Управление статусами (webhook)

Для отправки уведомлений о статусах транзакций внешним системам используется webhook. Для финализации статуса транзакции через webhook необходимо зафиксировать статусы completed, declined, error.

После финализации статуса сервис Accounts отправляет callback:

  • Во внешнее приложение - если статус терминальный;

  • В терминал - всегда, независимо от статуса (включая статус IN_PROGRESS, после этого терминал ожидает дальнейшие обновления по WebSocket).

Callback отправляется через API на backend терминала. В случае отсутствия ответа 200 от терминала, запрос должен повториться через failedCallbacks.

Актуализация статуса

Актуализация статуса происходит через запросы / WebSocket. Для получения информации по ордеру после завершения SAGA на стороне терминала реализован метод API getOrder. После отправки запроса на создание депозита будет возвращен статус IN_PROGRESS (без redirectUrl и paymentDetails, которые поступят позже по callback от сервиса Accounts и далее будут переданы терминалом на фронт по WebSocket. Метод API getOrder будет использоваться в случае необходимости ручного запроса или проверки.

Шаг 1: Создание ордера

  • Customer формирует запрос на создание ордера (через интерфейс ExternalApp);

  • ExternalApp отправляет запрос на создание платежа вызовом CreatePayment в Сервис Accounts;

  • В рамках flow по созданию ордера Сервис Accounts:

    • создает запись ордера (Accounts->Orders: createOrder);

    • сохраняет ордер в БД со статусом in_progress (Orders->Database: saveOrder(in_progress)) - получает Id ордера из таблицы orders;

    • записывает статус ордера в таблицу order_status_log (OrderStatusLog->Database: saveStatusLog);

    • записывает данные ордера в таблицу order_log (OrderLog->Database: saveOrderLog);

  • Сервис Accounts отправляет события в Kafka [topic] saga_orders_ready_to_validate.

Шаг 2: Валидация пользователя и транзакции

  • Сервис Blacklist проверяет пользователя;

  • Сервис Blacklist отправляет результаты валидации в Kafka [topic] saga_orders_validated;

  • Сервис Accounts получает результаты валидации из Kafka, и если:

    • статус одного из сервисов валидации (Blacklist или AntiFrod) = invalid, сервис Accounts обновляет статус ордера и инициирует callback в терминал через API. Callback содержит финальный статус и поле error (без redirectUrl или paymentDetails). В случае отсутствия ответа 200 от терминала, запрос должен повториться через failedCallbacks. После фиксации статус ордера на error по результату валидации = invalid одного из сервисов валидации последующие результаты валидации = invalid из других сервисов проверки не будут записываться.

    • статус всех сервисов валидации (Blacklist и AntiFrod) = validate, сервис Accounts действует в зависимости от типа платежа в соответствии с описанием Шаг 3.

! Внешнее приложение будет получать callback в любом случае, независимо от типа (H2H или H2C)

Шаг 3: Проведение платежей в зависимости от типа (H2H или H2C)

Единая модель для всех типов платежей (H2H, H2C)

Клиент всегда направляется на наш терминал, который отображает Preloader - пока SAGA выполняется (проверка валидности и запуск транзакции):

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

    • Если H2C - платеж переходит в iframe, клиент переходит через внешний шлюз для оплаты;

    • Если H2H - мы генерируем нужную форму для ввода реквизитов, клиент вводит данные в нашем терминале;

  • Терминал выполняет авторизацию;

  • Сервис Accounts отправляет запрос на обработку транзакции в Kafka [topic] saga_payments с обязательными полями согласно описания;

  • Сервис PaymentAggregator получает запрос из Kafka [topic] saga_payments и обращается к PaymentSystem (запрашивает redirectUrl для H2C или генерирует форму для ввода реквизитов для H2H);

  • Сервис PaymentAggregator получает результаты от PaymentSystem (подтверждение или redirectUrl) и передает в Kafka [topic] saga_transactions;

  • Сервис Accounts получает ответ через Kafka [topic] saga_transactions и пушит обновление статуса в терминал:

    • Accounts отправляет в Терминал Back данные - redirectUrl или paymentDetails + актуальный статуса ордера (эти данные отправляются терминалу по API через callback даже если статус не финализирован);

    • Terminal Back пушит полученные данные в Terminal Front через Websocket для отображения UI клиенту (если ответ от терминала не поступил - данные будут повторно отправлены через failedCallbacks).

Шаг 4: Результат проведения платежа

  • Сервис Accounts обновляет статус ордера в таблице order_status_log (Accounts->OrderStatusLog: updateStatus, OrderStatusLog->Database: saveStatus);

  • Сервис Accounts сохраняет информацию об ордере в таблицу order_log (Accounts->OrderLog: logSuccess, OrderLog->Database: saveLog);

  • Сервис Accounts отправляет callback с финальным статусом ордера;

  • Сервис Accounts передает терминалу redirectUrl или paymentDetails;

  • Терминал пушит данные на frontend через Websocket канал;

  • Frontend обновляет UI для клиента в зависимости от статуса (редирект на PaymentSystem через iframe для H2C или отображает форму с реквизитами для ввода данных для H2H) (может использовать запрос getOrder для получения актуального статуса в случае отсутствия Websocket).

Данные, которые backend терминала отправляет по WebSocket после получения события от сервиса Accounts:

Единый WebSocket-канал

Для обмена данными между терминалом и backend используется один общий WebSocket-канал для всех терминалов.

Терминал будет получать redirectUrl и paymentDetails только по callback от сервиса Accounts и передавать на фронт через WebSocket. Клиент получит на фронте статус IN_PROGRESS сразу, а платежные данные - асинхронно.

Безопасность и зрелость системы

Платформа прошла сертификацию PCI DSS версии 4, включая penetration testing, сканирование уязвимостей и анализ архитектуры. Это был сложный, но крайне полезный этап, который сильно повысил зрелость всей системы.

Область применимости
Область применимости
Поток платежных данных
Поток платежных данных

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

Вместо итога

Эта статья честный срез инженерных решений, которые мы принимали.

Нам будет особенно интересно услышать:

  • Где вы видите слабые места описанного подхода?

  • Какие решения вы приняли бы иначе?

  • и с какими проблемами масштабирования B2B-платформ вы сталкивались сами?

Хорошая архитектура редко рождается в одиночку — чаще всего она появляется из споров и сомнений.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как вы оцениваете модель B2B SaaS-маркетплейса, где платформа берёт на себя back-office (UI, доступы, биллинг, аудит), а вендоры развивают и хостят собственные backend-сервисы?
25%Да, модель выглядит жизнеспособной — как с продуктовой, так и с технической точки зрения1
25%Интересно, но вижу существенные риски — в lock-in, правилах экосистемы или дистрибуции1
25%Технически реализуемо, но рынок может не принять — сомневаюсь в готовности вендоров к такому формату1
0%Продуктовая идея понятна, но техническая сложность избыточна — те же задачи можно решить проще0
0%Не мой сегмент / сложно оценить — работаю с другими классами продуктов0
25%Скорее нет — не вижу практической ценности ни как вендор, ни как клиент1
Проголосовали 4 пользователя. Воздержавшихся нет.