Перейти к содержимому

27. Внешняя интеграция

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

Эта глава целиком моделирует одну такую интеграцию: платёжный сервис, который интегрируется со Stripe (платёжным процессором) для исходящих списаний и принимает вебхуки от Stripe для обновлений статуса. Компактно, но каждая забота — настоящая.

Что моделируем

Customer ──► Payments.Authorize
Payments ──► Stripe.Charge (исходящий, синхронный HTTP)
Stripe ──► Payments.PaymentWebhook (входящий, асинхронный)
Payments ──► Ledger.Record

Три модуля Archlang: Payments (ваш), Stripe (внешний), Ledger (ваш). Две границы — исходящая от Payments к Stripe и входящая от Stripe к Payments через вебхук.

Почему это отдельный разобранный пример

Здесь важны два паттерна, которые не возникли в чистом виде в главах 24-25:

  • Вид external_system, который заставляет указывать два обязательных поля (ext.vendor, ext.contract.url), так что внешние зависимости всегда документируют, что они такое и где живёт их контракт.
  • Вебхуки как входящие интерфейсы на вашем модуле, при этом внешняя система — вызывающий. Новички часто пытаются поместить обработчик вебхука на внешнюю сторону; это неверно — внешняя система доставляет вызов, ваш обработчик его принимает.

Если правильно осмыслить эти два пункта, моделирование интеграций становится прямолинейным. Остальная часть главы — пример.

Раскладка пакета

shop-payments/
├── package.archspace
├── payments.arch # Payments, Ledger
├── integrations.arch # Stripe (external_system)
├── processes.arch
└── views.arch

Манифест:

name: acme.payments
version: "0.1.0"
use * from arch.modules
use * from arch.kinds

Модули

payments.arch:

service Payments {
team: Payments
labels {
domain: Payments
security.zone: PCI
}
"Обработка карточных платежей. Авторизует через Stripe; принимает вебхуки для асинхронных исходов."
command Authorize {
"Авторизация со стороны клиента. Синхронно вызывает Stripe, возвращает токен холда."
}
command Capture {
"Захват ранее авторизованного холда."
}
command Refund {
"Возврат захваченного платежа."
}
command PaymentWebhook {
"Входящий вебхук от Stripe — payment_succeeded, payment_failed, refund.created.
Проверяется по HMAC-подписи запроса."
subscribes: Stripe.PaymentEvents
}
}
service Ledger {
team: Finance
labels {
domain: Finance
security.zone: Internal
}
"Неизменяемая финансовая запись."
command Record
}

Ключевой момент: command PaymentWebhook — обычная команда на Payments, с subscribes: Stripe.PaymentEvents. С точки зрения модели Stripe публикует события; Payments их обрабатывает. То, что эти события приходят через HTTP POST, который шлёт Stripe, — деталь реализации.

integrations.arch:

external_system Stripe {
team: External
ext.vendor: "Stripe"
ext.contract.url: "https://stripe.com/docs/api"
labels {
domain: Payments
security.zone: External
}
"Внешний карточный процессор. Исходящие HTTP-вызовы + входящие вебхуки."
command Charge {
"Исходящий. Сюда направляются авторизация/захват/возврат."
}
event PaymentEvents {
"Исходящий (с точки зрения Stripe) поток вебхуков.
Включает payment_succeeded, payment_failed, refund.created."
}
}

Три вещи, которые стоит заметить:

  1. ext.vendor и ext.contract.url обязательные. Они идут из типа external_system стандартной библиотеки. Команда не может просто так добавить внешнюю систему, не указав, что это такое и где её документация.
  2. Stripe.Charge — обычный command. С точки зрения архитектуры это синхронный интерфейс — форма вызова — даже если запускает её не ваша команда.
  3. Stripe.PaymentEvents — это event. На него подписываются ваши обработчики входящих вебхуков.

Ошибка, которой стоит избегать. Не моделируйте получатель вебхука как что-то на стороне Stripe («Stripe.WebhookSender» или подобное). Получатель вебхука — ваш, это интерфейс на вашем модуле. Сторона Stripe — просто источник событий. Асимметрия здесь настоящая: Stripe тоже не моделирует вас в своей архитектуре.

Процессы

processes.arch:

process #flow01 OutboundCharge {
Customer > Payments.Authorize
Payments > Stripe.Charge : "синхронный HTTP POST"
try {
Payments > Ledger.Record : "логируем холд"
} catch "declined" {
Payments > Ledger.Record : "логируем отказ"
}
}
process #flow02 InboundWebhook {
Stripe > Payments.PaymentWebhook : "асинхронная доставка с проверкой HMAC"
Payments > Ledger.Record
}

OutboundCharge отражает синхронную сторону — клиент инициирует, Payments вызывает Stripe, Stripe отвечает, результат попадает в Ledger. try/catch явно отделяет путь отказа.

InboundWebhook — асимметричный поток. Stripe — вызывающий; вызов попадает на наш обработчик PaymentWebhook. Следуя принципу из главы 7: вызывающий — это сущность, выполняющая вызов, то есть Stripe. Интерфейс живёт на получателе, Payments.

Проекции

views.arch:

view PaymentBoundary {
focus domain: Payments
group by team
layout elk
"Обе стороны границы Stripe, со входящим и исходящим потоками."
}
view ExternalSurface {
focus security.zone: External
layout dagre
"Все внешние системы, от которых мы зависим. Для ревью рисков по поставщикам."
}

ExternalSurface — это проекция, которая нужна тому, кто оценивает риски по поставщикам. Метка security.zone: External стоит на Stripe; проекция подхватывает её и любую другую внешнюю систему в рабочем пространстве.

Что это даёт

После валидации:

  • Диаграмма с тремя модулями — двумя вашими и Stripe в визуально отличном оформлении (внешние системы по умолчанию получают свой виджет).
  • Видны два потока процессов: исходящий синхронный платёж и входящий вебхук.
  • Подписка на PaymentWebhook, благодаря которой рендерер соединяет его с Stripe.PaymentEvents пунктирным ребром.
  • Проекция «поверхности поставщиков», автоматически наполняемая по мере добавления новых внешних систем.

Паттерны для более богатых интеграций

В этом примере одна внешняя система, один исходящий интерфейс, одно событие. Реальные интеграции бывают разными:

Несколько операций на одного поставщика. У Stripe 50+ конечных точек API. Моделируйте те, которые важны для вашей архитектуры, — обычно 3-8. Остальные не обязаны появляться, это детали реализации поставщика.

Маршрутизация вебхуков через одну точку. Многие системы принимают все вебхуки на один URL и диспетчеризуют внутри. Это всё равно моделируется как несколько интерфейсов — диспетчеризация это реализация. С точки зрения модели существует N потоков вебхуков.

Системы с заменяемым поставщиком. Plaid, Stripe, Adyen — вы можете поддерживать несколько провайдеров. Используйте проектный вид-тип вроде card_processor, который фиксирует контракт; экземпляры вставляют в него конкретных поставщиков. Тема главы 29.

Провайдеры идентификации. Auth0, Okta, внутренний SSO. Тот же паттерн, что и Stripe: external_system с command Login, event UserEvents для событий вебхуков. Форма не меняется с поставщиком.

Только асинхронные интеграции. Некоторые поставщики только отправляют (вебхуки, без вызываемого API). Просто объявите интерфейс event и получателя; пропустите исходящий command. Модель прекрасно это обрабатывает.

Дисциплина границы

Единственное правило, которое стоит усвоить:

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

Эта фраза — вся глава. Если вы можете ответить «кто вызывает?», вы можете смоделировать любую интеграцию. Грамматика (Caller > Callee.Interface) и вид external_system делают остальное.

Итоги

  • Используйте external_system для всего, что вне вашей операционной границы; обязательные ext.vendor и ext.contract.url поддерживают честность документации.
  • Получатели вебхуков — это интерфейсы на вашем модуле; внешняя система — вызывающий.
  • Подписка (subscribes: External.Events) соединяет ваш обработчик с их потоком событий.
  • Моделируйте 3-8 интерфейсов на поставщика — те, с которыми общается ваша архитектура, а не весь API поставщика.
  • Проекции рисков по поставщикам сами получаются из focus security.zone: External, как только у внешних систем стоит эта метка.

Что дальше

Глава 28: Границы соответствия требованиям → — метки, проекции и правила валидации, работающие вместе как механизм управления.