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

25. SaaS-бэкенд

Это первый из шести разобранных проектов в части VI. Каждая глава берёт конкретную форму системы и моделирует её с нуля — модули, интерфейсы, процессы, проекции и (где это уместно) типы. Цель не в том, чтобы научить возможностям, описанным в предыдущих главах; цель — показать, как полная модель выглядит на практике и какие решения вы будете принимать, когда будете строить свою.

Система этой главы: небольшой SaaS-бэкенд. Аутентификация, управление заказами, платежи, уведомления. В основном синхронные вызовы с асинхронным событийным позвоночником. Шесть модулей, четыре процесса, две проекции. Примерно размер продуктового бэкенда стартапа на ранней стадии.

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

Customer ──► Auth (Login, GetSession)
Customer ──► Orders (CreateOrder, GetOrder, CancelOrder)
Orders ──► Inventory (Reserve, Release)
Orders ──► Payments (Authorize, Capture, Refund)
Payments ──► Ledger (Record)
Orders ──► OrderEvents (event interface)
Notifications subscribes to Orders.OrderEvents

Два домена: коммерция (Orders, Inventory, Payments, Ledger) и платформа (Auth, Notifications). Один асинхронный переход — события заказов на Orders расходятся к обработчику на Notifications.

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

shop/
├── package.archspace
├── platform.arch # Auth, Notifications
├── commerce.arch # Orders, Inventory, Payments, Ledger
├── processes.arch # the four end-to-end flows
└── views.arch # the two saved views

Манифест:

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

Два файла модулей, один с процессами, один с проекциями. Разбиение по доменам держит каждый файл меньше 100 строк.

Модули

platform.arch:

service Auth {
team: Platform
labels {
domain: Platform
security.zone: Internal
}
"Authentication and session management."
command Login {
"Verify credentials and create a session token."
}
query GetSession {
"Look up a session by token."
}
event SessionEvents {
"Emitted when sessions are created, refreshed, or revoked."
}
}
service Notifications {
team: Platform
labels {
domain: Platform
security.zone: Internal
}
"Sends transactional email, SMS, and push notifications."
command SendEmail {
"Sent on OrderPaid and OrderCancelled events."
subscribes: Orders.OrderEvents
}
command SendSMS
command SendPush
}

commerce.arch:

service Orders {
team: Commerce
labels {
domain: Commerce
security.zone: Internal
}
"Order lifecycle — placement, cancellation, history."
command CreateOrder { "Place a new order. Returns an order ID." }
command CancelOrder { "Cancel an order before fulfillment." }
query GetOrder { "Look up an order by ID." }
event OrderEvents { "Lifecycle events: placed, paid, fulfilled, cancelled." }
}
service Inventory {
team: Commerce
labels {
domain: Commerce
security.zone: Internal
}
"Stock counts and reservation state."
command Reserve { "Reserve units against an order." }
command Release { "Release a prior reservation." }
query Check { "Check current available stock for a SKU." }
}
service Payments {
team: Payments
labels {
domain: Payments
security.zone: PCI
}
"Card payment processing — authorize, capture, refund."
command Authorize { "Authorize a payment hold." }
command Capture { "Capture an authorized payment." }
command Refund { "Issue a refund." }
}
service Ledger {
team: Finance
labels {
domain: Finance
security.zone: Internal
}
"Immutable financial record of every transaction."
command Record { "Record a financial event." }
}

Шесть модулей. Обратите внимание на распределение команд: Platform, Commerce, Payments, Finance. Обратите внимание на метки — domain и security.zone каскадируют на каждый интерфейс внутри; мы обопрёмся на это в проекциях.

Ошибки, которых стоит избегать. Не пытайтесь моделировать “аутентификацию”, добавляя поле requiresAuth: true к каждой команде. Аутентификация — это забота вызывающей стороны (актора или вызывающего в процессе), а не получателя. Модель фиксирует кто кому звонит через процессы; несёт ли этот вызов токен сессии — это реализация, а не архитектура.

Процессы

processes.arch:

process #flow01 SessionStart {
Customer > Auth.Login
Auth > Auth.SessionEvents
}
process #flow02 Checkout {
Customer > Auth.GetSession : "validate session"
Customer > Orders.CreateOrder
Orders > Inventory.Reserve
Orders > Payments.Authorize
switch "payment outcome" {
authorized {
Orders > Payments.Capture
Payments > Ledger.Record
Orders > Orders.OrderEvents : "OrderPaid"
}
declined {
Orders > Inventory.Release
Orders > Orders.OrderEvents : "OrderFailed"
}
}
}
process #flow03 Cancellation {
Customer > Auth.GetSession
Customer > Orders.CancelOrder
Orders > Inventory.Release
Orders > Payments.Refund
Payments > Ledger.Record
Orders > Orders.OrderEvents : "OrderCancelled"
}

Checkout — центральный бизнес-поток. switch "payment outcome" фиксирует двухстороннее ветвление при авторизации — каждая case-метка (authorized, declined) именует ветку. Метки после двоеточия (: "validate session", : "OrderPaid") аннотируют шаги для читаемости диаграммы, не меняя семантику.

Разветвление уведомлений разведено декларативно — Notifications.SendEmail несёт subscribes: Orders.OrderEvents, которое живёт на декларации интерфейса в platform.arch. Явный процесс не нужен; рендерер диаграммы соединит событие с его подписчиком.

Альтернатива: стиль явного процесса. Если вы предпочитаете видеть разветвление как процесс ради лучшей обнаружимости на ревью, уберите subscribes: и добавьте:

process NotificationFanout {
Notifications > Notifications.SendEmail : "on OrderEvents"
}

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

Проекции

views.arch:

view CustomerJourney {
focus domain: Commerce
focus domain: Platform
include "Auth.*", "Orders.*", "Inventory.*", "Payments.*", "Notifications.*"
group by team
layout elk
"End-to-end customer journey. Used in onboarding and architecture reviews."
}
view PCIScope {
focus security.zone: PCI
group by team
layout dagre
"Every service in PCI scope. For compliance review."
}

Две проекции. Первая — путь клиента целиком, сгруппированный по командам, — то, что вы показали бы при введении в проект. Вторая изолирует подмножество, входящее в зону PCI, для compliance-ревью; с расставленными метками это буквально focus security.zone: PCI, остальное делает рендерер.

Решения, с которыми вы столкнётесь, моделируя своё

Гранулярность модулей. Должен ли Orders быть одним модулем или несколькими? Правило: если у него одна команда и одна граница — один модуль. Команда может разбить его внутри через под-модули component позже (см. стандартный вид component в Главе 11) без изменения внешней поверхности.

Sync vs async. Используйте command и query для синхронных вызовов; используйте eventsubscribes: на обработчике) для асинхронных. Смешивать их в одном интерфейсе неправильно — вид передаёт форму вызова.

Где располагать метки. Метки идут на тот контейнер, который лучше всего описывает область классификации. domain и security.zone принадлежат модулям. criticality может принадлежать отдельным интерфейсам (у сервиса может быть одна критичная команда и двадцать справочных запросов). Если сомневаетесь — поставьте на более широкую область и переопределите вниз.

Гранулярность процессов. Один процесс на один бизнес-поток. Если у потока есть чистое условное разделение, используйте if или switch. Если есть независимая параллельная работа, используйте parallel. Используйте try / catch для явных путей ошибок. Не пытайтесь закодировать каждый путь ошибки; фиксируйте счастливый путь и основные альтернативы — исчерпывающая обработка — это работа валидатора, а не модели.

Внешние акторы. Customer здесь — actor из стандартной библиотеки. Для B2B-интеграций или сторонних вызывающих объявляйте их как external_system или actor и ставьте слева от шагов процесса. Они никогда не появляются справа (потому что они не выставляют интерфейсы вашей модели).

Что это вам даёт

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

  • Диаграмма с шестью сервисными узлами и актором, рёбра выведены из четырёх процессов, группировка по командам.
  • Вторая диаграмма, отфильтрованная по зоне PCI.
  • Валидация того, что каждый шаг процесса ссылается на реальный интерфейс.
  • Движок диффов, распознающий переименования, если вы, скажем, позже переименуете Orders в OrderService.
  • Описания с markdown во всплывающих подсказках, с кросс-ссылками [[Notifications]] и интерполяцией @security.zone, если вы её добавите.

Итого исходник: примерно 120 строк .arch в четырёх файлах. Это вся форма системы, в тексте, в системе контроля версий.

Итог

  • Моделируйте свой домен, сначала записывая модули, затем рисуя процессы между ними. Стрелки на диаграмме появятся следом.
  • Группируйте модули по командам и по доменам через метки; проекции разрезают модель по этим осям.
  • Используйте command/query для синхронных вызовов, event+subscribes: для асинхронных; выбирайте между стилем явных процессов и разводкой через подписки исходя из того, что яснее в вашей кодовой базе.
  • От трёх до шести модулей на файл, разбиение по доменам — комфортная плотность.
  • Проекция PCI получается автоматически, как только security.zone: PCI стоит на нужных модулях — метки делают работу.

Что дальше

Глава 26: Событийный конвейер → — другая топология: очереди, потоки, разветвление и сборка, разводка через подписки как основной механизм интеграции.