27. Внешняя интеграция
Реальные системы не работают в изоляции. Они вызывают платёжные процессоры, обращаются к провайдерам идентификации, принимают вебхуки от инструментов аналитики, передают запросы на доставку перевозчикам. Архитектурные решения на границе — какие вызовы пересекают ваш периметр, что приходит обратно, что моделировать, а что оставить за бортом — отличаются от всего, о чём шла речь в двух предыдущих главах.
Эта глава целиком моделирует одну такую интеграцию: платёжный сервис, который интегрируется со Stripe (платёжным процессором) для исходящих списаний и принимает вебхуки от Stripe для обновлений статуса. Компактно, но каждая забота — настоящая.
Что моделируем
Customer ──► Payments.AuthorizePayments ──► 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.paymentsversion: "0.1.0"
use * from arch.modulesuse * 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." }}Три вещи, которые стоит заметить:
ext.vendorиext.contract.urlобязательные. Они идут из типаexternal_systemстандартной библиотеки. Команда не может просто так добавить внешнюю систему, не указав, что это такое и где её документация.Stripe.Charge— обычныйcommand. С точки зрения архитектуры это синхронный интерфейс — форма вызова — даже если запускает её не ваша команда.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: Границы соответствия требованиям → — метки, проекции и правила валидации, работающие вместе как механизм управления.