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Манифест:
package: acme.paymentsversion: "0.1.0"
// Выбираем рабочий набор явно — никогда не делайте `use *` по стандартной// библиотеке (это затягивает всю поверхность и связывает вас с типами,// которыми вы не пользуетесь).use service, external_system, rest_create, webhook from arch.backendКорневой манифест объявляет package: (непрозрачную единицу разрешения имён); name: нужен для вложенных пространств, которые интеграции на одну систему вроде этой не требуются. Список use — это явная, легко обнаруживаемая палитра для всего пакета: любой, кто открыл манифест, видит ровно тот словарь, что в игре.
payments.arch:
service Payments { aspect team: "Payments"
repo.url: "https://github.com/acme/payments" ext.runbook.url: "https://wiki.acme.com/payments-runbook" ext.console.url: "https://dashboard.stripe.com/acme"
aspect { domain: "Payments" security.zone: "PCI" }
"Card payment processing. Authorizes via Stripe; receives webhooks for async outcomes."
rest_create authorize { "Customer-facing authorize. Calls Stripe synchronously, returns a hold token." }
rest_create capture { "Capture a previously authorized hold." }
rest_create refund { "Refund a captured payment." }
webhook paymentWebhook { "Inbound webhook from Stripe — payment_succeeded, payment_failed, refund.created. Verified by HMAC signature on the request." }}
service Ledger { aspect team: "Finance"
repo.url: "https://github.com/acme/ledger"
aspect { domain: "Finance" security.zone: "Internal" }
"Immutable financial record."
rest_create record}Ключевой момент: webhook paymentWebhook — это асинхронный интерфейс-обработчик на Payments (webhook асинхронен). На нём не объявлена подписка — соединение делает ребро процесса Stripe > Payments.paymentWebhook (ниже); ребро и есть подписка. С точки зрения модели поведение Stripe запускает обработчик; то, что оно приходит через HTTP POST, — деталь реализации.
Обратите внимание на поля repo.url, ext.runbook.url и ext.console.url. Структурированные внешние ссылки место в полях, а не в описании — они превращают модуль в узел, который направляет читателя к настоящему репозиторию, ранбуку и консоли поставщика.
Правило. Делайте каждый модуль узлом ссылок. Платёжный сервис должен указывать на свой репозиторий, свой ранбук и дашборд поставщика. Ссылки — это дешёвое поэлементное украшение, и именно здесь модель оправдывает себя в повседневной работе.
Правило.
security.zoneздесь — это уровень соответствия требованиям (PCI / External / Internal), а не сетевое размещение. Сетевая плоскость — в каком сегменте модуль физически находится и какие сегменты могут достучаться до каких — это отдельная аспектная плоскость (network-zone), которую мы моделируем в главе 28. Держите эти две плоскости раздельно.
integrations.arch:
external_system Stripe { aspect team: "External"
ext.vendor: "Stripe" ext.contract.url: "https://stripe.com/docs/api" ext.console.url: "https://dashboard.stripe.com"
aspect { domain: "Payments" security.zone: "External" }
"External card processor. Outbound HTTP calls + inbound webhooks."
rest_create charge { "Outbound. authorize/capture/refund routed here." }}Три вещи, которые стоит заметить:
ext.vendorиext.contract.urlобязательные. Они идут из типаexternal_systemстандартной библиотеки. Команда не может просто так добавить внешнюю систему, не указав, что это такое и где её документация.Stripe.charge— обычныйrest_create. С точки зрения архитектуры это синхронный интерфейс — форма вызова — даже если запускает её не ваша команда.- Входящие события не объявляют интерфейс на Stripe. Сторона Stripe — просто источник событий; ваш обработчик
webhook paymentWebhook— место, куда они приземляются. Нет ни интерфейса события, который можно было бы повесить на Stripe, ни поляsubscribes:— входящий поток — это единственное ребро процессаStripe > Payments.paymentWebhook.
Ошибка, которой стоит избегать. Не моделируйте получатель вебхука как что-то на стороне Stripe («Stripe.webhookSender» или подобное). Получатель вебхука — ваш, это интерфейс на вашем модуле. Сторона Stripe — просто источник событий. Асимметрия здесь настоящая: Stripe тоже не моделирует вас в своей архитектуре.
Процессы
Заголовок раздела «Процессы»processes.arch:
process #c3v7kd OutboundCharge { Customer > Payments.authorize Payments > Stripe.charge "synchronous HTTP POST"
try { Payments > Ledger.record "log the hold" } catch "declined" { Payments > Ledger.record "log the decline" }}
process #n5b1qw InboundWebhook { Stripe > Payments.paymentWebhook "HMAC-verified async delivery" Payments > Ledger.record}OutboundCharge отражает синхронную сторону — клиент инициирует, Payments вызывает Stripe, Stripe отвечает, результат попадает в Ledger. try/catch явно отделяет путь отказа.
InboundWebhook — асимметричный поток. Stripe — вызывающий; вызов попадает на наш обработчик paymentWebhook. Следуя принципу из главы 7: вызывающий — это сущность, выполняющая вызов, то есть Stripe. Интерфейс живёт на получателе, Payments.
Проекции
Заголовок раздела «Проекции»views.arch:
view PaymentBoundary { "Both sides of the Stripe boundary, with the inbound and outbound flows." show @@domain:"Payments" group by @@team}
view ExternalSurface { "Every external system we depend on. For vendor-risk review." show @@security.zone:"External"}ExternalSurface — это проекция, которая нужна тому, кто оценивает риски по поставщикам. Аспект security.zone: "External" стоит на Stripe; проекция подхватывает её и любую другую внешнюю систему в рабочем пространстве.
Что это даёт
Заголовок раздела «Что это даёт»После валидации:
- Диаграмма с тремя модулями — двумя вашими и Stripe в визуально отличном оформлении (внешние системы по умолчанию получают свой виджет).
- Видны два потока процессов: исходящий синхронный платёж и входящий вебхук.
- Входящий вебхук, нарисованный как пунктирное асинхронное ребро от
StripeкpaymentWebhook, выведенное из процессаInboundWebhook— полеsubscribes:не нужно. - Проекция «поверхности поставщиков», автоматически наполняемая по мере добавления новых внешних систем.
Паттерны для более богатых интеграций
Заголовок раздела «Паттерны для более богатых интеграций»В этом примере одна внешняя система, один исходящий интерфейс, одно событие. Реальные интеграции бывают разными:
Несколько операций на одного поставщика. У Stripe 50+ конечных точек API. Моделируйте те, которые важны для вашей архитектуры, — обычно 3-8. Остальные не обязаны появляться, это детали реализации поставщика.
Маршрутизация вебхуков через одну точку. Многие системы принимают все вебхуки на один URL и диспетчеризуют внутри. Это всё равно моделируется как несколько интерфейсов — диспетчеризация это реализация. С точки зрения модели существует N потоков вебхуков.
Системы с заменяемым поставщиком. Plaid, Stripe, Adyen — вы можете поддерживать несколько провайдеров. Определите проектный тип вроде card_processor, который фиксирует контракт (обязательный поставщик, URL контракта, эндпоинт вебхука), и вставляйте в него поставщиков. Такой тип оправдывает себя за счёт настоящих полей required и общего виджета — а не пустой обёрткой. Тема главы 29.
Провайдеры идентификации. Auth0, Okta, внутренний SSO. Тот же паттерн, что и Stripe: external_system с rest_create login плюс асинхронный интерфейс-обработчик на вашей стороне (webhook userWebhook), который соединяет ребро процесса (Auth0 > You.userWebhook). Форма не меняется с поставщиком.
Только асинхронные интеграции. Некоторые поставщики только отправляют (вебхуки, без вызываемого API). Просто объявите асинхронный интерфейс-обработчик на вашем получателе и входящее ребро процесса; пропустите исходящий синхронный вызов. Модель прекрасно это обрабатывает.
Дисциплина границы
Заголовок раздела «Дисциплина границы»Единственное правило, которое стоит усвоить:
Вызовы заходят в ваш периметр; вы их принимаете. Вызовы уходят из вашего периметра; вы их инициируете. Всегда моделируйте того, кто вызывает; получатель — это всегда тот, чей интерфейс вызван.
Эта фраза — вся глава. Если вы можете ответить «кто вызывает?», вы можете смоделировать любую интеграцию. Грамматика (Caller > Callee.Interface) и тип external_system делают остальное.
- Используйте
external_systemдля всего, что вне вашей операционной границы; обязательныеext.vendorиext.contract.urlподдерживают честность документации. - Получатели вебхуков — это асинхронные интерфейсы-обработчики на вашем модуле; внешняя система — вызывающий.
- Ребро процесса (
External > You.handler) соединяет их push с вашим асинхронным обработчиком — поляsubscribes:нет; ребро и есть подписка. - Моделируйте 3-8 интерфейсов на поставщика — те, с которыми общается ваша архитектура, а не весь API поставщика.
- Проекции рисков по поставщикам сами получаются из
show @@security.zone:"External", как только у внешних систем стоит этот аспект. - Украшайте каждый модуль его реальными ссылками (
repo.url,ext.runbook.url,ext.console.url) как полями — модель это узел, а не мёртвая картинка. security.zone— это уровень соответствия требованиям; сетевая плоскость (network-zone) отдельна — см. главу 28.
Что дальше
Заголовок раздела «Что дальше»Глава 28: Границы соответствия требованиям → — аспекты, проекции и правила валидации, работающие вместе как механизм управления.