Перейти к содержанию

Macro-Architecture: топология деплоя

Верхний слой system design: как система разбита на deployable units — приложения, сервисы, процессы, базы, очереди, внешние интеграции — как эти части общаются и где развёрнуты. Не про устройство кода одного сервиса (это micro-architecture), а про то, сколько у тебя исполняемых единиц и где между ними проходит сеть.

Каталог идёт по одной оси — от одного процесса к распределённой системе. Сначала сама ось и её цена, потом каждый стиль, потом как части общаются, как это эволюционирует и что в итоге брать.


Карта: ось монолит ↔ распределённость

Все макро-стили лежат на одной оси. Левый конец — один deployable unit, monolith: весь код в одном процессе, один артефакт, один деплой. Правый — много распределённых units: microservices, serverless, EDA. Между концами — промежуточные точки, не отдельные вселенные. Конкретные стили разбираем дальше по каталогу, здесь — сама ось и её главный trade-off.

Главный trade-off: внутрипроцессный вызов vs сетевой

Сдвиг вправо — это замена вызовов функций на вызовы по сети. Один обмен, две стороны медали.

Внутрипроцессный вызов (левый конец): - быстрый — наносекунды, без сериализации и round-trip. - надёжный — функция не отвалится по таймауту, нет частичных отказов. - ACID-транзакция через границы модулей — один commit, один rollback, согласованность из коробки. - простой деплой и debug — один артефакт, один стектрейс, обычный дебаггер.

Сетевой вызов (правый конец): - масштабируемо — каждый unit скейлится отдельно, под свою нагрузку. - автономно — деплоишь и релизишь units независимо, команды не блокируют друг друга. - но сеть ненадёжна — latency, таймауты, частичные отказы, retry, идемпотентность. - ACID через границы units почти недостижим — вместо одной транзакции eventual consistency и saga с компенсациями. - debug распределённый — единого стектрейса нет, нужны distributed tracing и корреляция логов.

Грубо говоря: левый конец платит связностью, правый — эксплуатацией и согласованностью. Бесплатного сдвига нет.


Спектр

Точки на оси — по числу и автономии deployable units, слева направо:

   один процесс, ACID, простой деплой/debug              много units, сеть, eventual consistency
   <=============================================================================================>

   Monolith --> Modular Monolith --> SCS --> SOA --> Microservices --> Serverless/FaaS

   вправо: больше автономии и scale-out
   дороже эксплуатация и согласованность

Что меняется по мере движения вправо: - Monolith — один deployable unit, всё в процессе, границы модулей опциональны. - Modular Monolith — всё ещё один unit, но внутри жёсткие границы модулей. Граница есть, сети нет. Подробнее — микросервисы. - SCS — несколько крупных вертикальных срезов, каждый со своим UI и данными, интеграция через UI или асинхронно. - SOA — функциональность как набор сетевых services с контрактами, часто координация через общий middleware. - Microservices — много мелких автономных services, у каждого свой процесс и свой store, общение по сети. Про сам обмен — Communication & API Style. - Serverless/FaaS — короткоживущие event-triggered функции без управления серверами, скейлятся провайдером.

Note

Спектр — про deployment topology, не про качество кода. Modular Monolith слева может быть чище любых криво нарезанных microservices справа. Монолит — это про упаковку в один unit, а не про грязный код.


Что двигает вправо — и что нет

Сдвиг вправо диктуют две вещи, не мода.

Организация (закон Конвея): - система повторяет коммуникационную структуру организации, которая её строит. - много автономных команд → много автономных units почти неизбежно. Один deployable unit на десять команд превращается в bottleneck релизов. - это эмпирическое наблюдение Конвея из статьи 1968 года, не предписание. Но игнорировать дорого.

Масштаб: - разные части системы скейлятся по-разному — выноси горячий кусок в отдельный unit под свою нагрузку. - пик на одной функции не должен заставлять скейлить весь монолит целиком.

Что НЕ повод двигаться вправо: - «микросервисы — это современно». Каждый сетевой вызов покупаешь ценой надёжности и согласованности. - greenfield по умолчанию. Дефолт обычно обратный — MonolithFirst: начинай монолитом, режь по сервисам, когда границы понятны. - ошибка дорогая в обе стороны. Поспешил вправо без понимания границ — получишь distributed monolith: units много, а деплоятся и меняются всё равно вместе. Худшее из обоих миров.


Стили топологии

Каждый стиль — точка или область на оси из карты выше. Сначала монолитные (один deployable), потом распределённые (SOA, microservices, SCS, serverless, EDA), рядом — BFF как обвязка под клиентов и distributed monolith как предостережение.

Монолит и Three-Tier

Monolith — это приложение, которое деплоится как один deployable unit: весь код в одной кодовой базе, собирается в один артефакт, крутится в одном process.

Что это значит на практике: - Один runtime, один deployment. Выкатываешь — выкатываешь всё целиком, одной командой. - Все вызовы между частями — внутрипроцессные. Вызов функции, а не запрос по сети. - Обычно одна БД на всё приложение. Отсюда настоящие ACID-транзакции из коробки. - Граница модулей — логическая (пакеты, неймспейсы), а не физическая. Ничто не мешает одному куску дёрнуть другой напрямую.

Ключевое: monolithic — это про packaging и deployment, не про качество кода. Чисто структурированный монолит с явными границами модулей — это modular monolith (см. микросервисы), а не оксюморон. «Монолит» не равно «помойка в одном файле».


Three-Tier как раскладка монолита

Three-Tier — это физическая раскладка, где приложение разбито на три tier: presentation (UI), application (бизнес-логика), data (хранилище), и tier'ы могут жить на разных узлах.

Откуда термин: ранний client-server, начало 1990-х, без одного канонического автора. Tier — это физическая граница деплоя, поэтому каждый tier масштабируется и обновляется относительно независимо.

Типичная топология:

   [ browser / client ]
            |
        HTTP/RPC
            |
   +-----------------+      presentation tier
   |   web server    |      (отдаёт UI, статику)
   +-----------------+
            |
        внутри сети
            |
   +-----------------+      application tier
   |   app server    |      (бизнес-логика, монолит-процесс)
   +-----------------+
            |
       SQL / driver
            |
   +-----------------+      data tier
   |    database     |      (одна БД, ACID)
   +-----------------+

Тонкость, на которой все спотыкаются: tier — это не layer. - tier — физическая граница: отдельный процесс/узел, общение по сети. - layer — логическая граница: слои кода (presentation/domain/data-source) внутри одного process.

Layered architecture уровня кода (Shaw & Garlan, 1996; Fowler, PoEAA, 2002) живёт целиком внутри одного deployable. Three-tier — про топологию деплоя. Один app-tier монолит часто имеет внутри три layer'а кода — это нормально и не делает его «трёхзвенным» в смысле распределённости. Слойную архитектуру уровня кода разбираешь отдельно — это про организацию модулей, не про узлы.


Миф «монолит = legacy»

Расхожее заблуждение: монолит — это что-то устаревшее, что приличные люди давно поделили на микросервисы. Это неправда. Для большинства систем монолит — разумный дефолт, а не компромисс.

Сам Fowler сформулировал это как MonolithFirst (2015): новое приложение обычно лучше начинать монолитом и резать на сервисы только когда границы реально понятны. Пока их не нащупал, распил по сети — это фиксация неверных границ в самом дорогом виде.

Канон vs индустрия: - Индустрия второй половины 2010-х продавала микросервисы как дефолт для greenfield. - Канон (Fowler, Newman) — ровно наоборот: монолит первым, сервисы по нужде. - «Монолит vs микросервисы» — это не «плохо vs хорошо». Это разные точки на кривой сложность/масштаб.


Плюсы и минусы

Плюсы: - Простой деплой — один артефакт, один pipeline, один rollback. - ACID-транзакции внутри одной БД. Не нужны saga, eventual consistency, distributed transactions. - Нет сети между частями — нет network latency, нет partial failure, нет сериализации на каждом вызове. Звонок функции либо отработал, либо кинул исключение. - Легко рефакторить через границы — IDE видит весь код, переименование и перенос логики безопасны на уровне компилятора. - Проще дебажить — один стектрейс, один лог, один процесс под профайлером.

Минусы: - Масштабируется только целиком. Узкое место в одном модуле — реплицируешь весь процесс, даже если 90% кода в репликах простаивает. - Один релиз на всех. Любая правка — выкатка всего приложения; крупный артефакт деплоится медленнее. - При росте команды — затор. Десятки людей в одной кодовой базе, общий pipeline, конфликты на мерже, страх задеть чужое. - Размывание границ. Раз ничто не мешает дёрнуть соседний модуль напрямую — со временем модули срастаются в большой ком (тот самый «big ball of mud»). - Технологический lock-in. Один runtime на всё — не выберешь другой язык под отдельный кусок.

Цена: - Платишь связностью внутри процесса в обмен на отсутствие сложности распределённой системы. Грубо говоря, монолит переносит сложность из runtime (сеть, консистентность, координация) в кодовую базу (дисциплина границ). Вторую держать дешевле, пока команда и домен влезают в одну голову.


Когда подходит

Когда подходит: - Greenfield и стартапы — границы домена ещё не ясны, скорость итерации важнее изоляции. - Небольшая команда (грубо до 10-15 человек), которой хватает одного pipeline без заторов. - Нагрузка укладывается в вертикальное масштабирование или несколько реплик целого процесса. - Сильные транзакционные инварианты — когда проще держать ACID в одной БД, чем городить saga по сети.

Когда уже жмёт: - Команда разрослась, общий релиз стал узким местом, мерж-конфликты съедают время. - Разным частям нужны разные профили масштабирования или разные технологии. - Тогда смотришь в сторону modular monolith (граница без сети, см. микросервисы) — и только потом, по реальной нужде, в распределённые варианты (см. Communication & API Style про request/response vs события).


Modular Monolith

Один deployable unit, как обычный monolith. Но внутри он нарезан на модули, и модуль — это архитектурная граница, а не просто папка. Идея популяризирована Simon Brown (разговоры про modular monolith с 2015, доклад «Modular Monoliths» на Devoxx 2016); тот же сюжет — «The Missing Chapter» в «Clean Architecture» Роберта Мартина (2017).

Что это значит

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

Грубо говоря, тот же bounded context, но живёт внутри одного процесса, а не за сетью.

Чем отличается от обычного монолита

Граница. В обычном монолите слои и пакеты — это соглашение: «договорились не звать напрямую». Договорённость держится ровно до первого дедлайна. Дальше любой класс зовёт любой, и через год получаешь big ball of mud, где всё связано со всем.

В modular monolith граница принудительная. Не «не зови», а «не сможешь позвать». - видимость пакетов / модулей языка — internal в Go, module-info в Java, отдельные сборки в .NET - линтер или ArchUnit-тест валит билд, если модуль полез не туда - отдельный артефакт на модуль, и в публичном API торчит только интерфейс

Канон vs индустрия: - частая ошибка — считать modular monolith «монолитом с папочками» или обязательным переходным этапом к микросервисам - определяющая черта — именно enforced boundaries, а не аккуратная структура каталогов - это валидная конечная точка сама по себе, а не временная остановка по дороге в микросервисы


Топология

Всё в одном процессе. Модули зовут друг друга через свои API — это обычный вызов функции in-process, без сети, без сериализации.

            ┌─────────────────────────────────────────────┐
            │              один deployable unit            │
            │                 (один процесс)               │
            │                                              │
            │   ┌─────────┐   ┌─────────┐   ┌─────────┐    │
            │   │ Orders  │   │ Billing │   │ Catalog │    │
            │   │  ─────  │   │  ─────  │   │  ─────  │    │
            │   │  API    │◄──┤  API    │──►│  API    │    │
            │   │ (public)│   │ (public)│   │ (public)│    │
            │   ├─────────┤   ├─────────┤   ├─────────┤    │
            │   │ internal│   │ internal│   │ internal│    │
            │   │ (скрыто)│   │ (скрыто)│   │ (скрыто)│    │
            │   └────┬────┘   └────┬────┘   └────┬────┘    │
            │        │             │             │         │
            │     ┌──┴──┐       ┌──┴──┐       ┌──┴──┐      │
            │     │ orders│     │ bill │      │ cat  │     │
            │     │ schema│     │schema│      │schema│     │
            │     └───────┘     └──────┘      └──────┘     │
            │                  one database                │
            └─────────────────────────────────────────────┘

Связь in-process — это и есть ключевое отличие от микросервисов. Вызов соседнего модуля стоит как обычный method call: наносекунды, без network latency, без частичных отказов на ровном месте. Один деплой, одна транзакция базы поверх всех модулей, если схемы в одной СУБД.

Зачем

Изоляция уровня микросервисов без распределённой системы. - получаешь чёткие bounded context и низкий coupling между ними - не получаешь сеть, сериализацию, distributed transactions, eventual consistency и остальной налог на распределённость (подробнее про этот налог — Communication & API Style) - швы готовы заранее: если модуль реально надо вынести в service — граница уже есть, выносишь API за сеть, а не распутываешь клубок

Плюсы: - одна транзакция, один деплой, один процесс — отлаживать и тестировать на порядок проще - рефакторинг границ дешёвый: переставить вызов внутри процесса легко, переехать через сетевой контракт — больно - скейлится как обычный monolith: scale-out копиями всего процесса за балансировщиком

Минусы: - скейлить модули по отдельности нельзя — масштабируешь весь процесс целиком - общий runtime: память, GC, один упавший поток роняют всё приложение - общий релизный поезд — все модули едут одним деплоем, командам не порелизиться независимо

Грабли

Границы текут, если их не принуждать. Это главная и почти единственная серьёзная ловушка. - пока модуль торчит наружу всеми классами — кто-нибудь да позовёт напрямую, мимо API - «временный» прямой вызов в обход контракта живёт дольше любого сотрудника - общая база — частая дыра: формально модули изолированы, а по факту лезут в чужие таблицы JOIN-ом - лечится только машинно: видимость на уровне языка, ArchUnit/линтер в CI, отдельные артефакты — чтобы нарушение не компилировалось, а не ловилось на ревью

Ещё одна засада — путать модуль-как-границу с модулем-как-папку. Папка ничего не принуждает. Если выкинуть enforcement, через год снова big ball of mud, просто с красивым деревом каталогов.

Modular monolith — естественный дефолт для нового проекта и часто конечная точка, а не пересадочная станция. Микросервисы выделяешь из готовых модулей потом и только если припрёт — про микро-уровень самой модульности (пакеты, компоненты внутри модуля) смотри микросервисы.


SOA (Service-Oriented Architecture)

SOA — это стиль, где функциональность приложения выставлена как набор сетевых сервисов с явными контрактами, обычно поверх общей middleware. Канон — Gartner, Yefim Natis и Roy Schulte, 1996. Идея старше микросервисов почти на двадцать лет: разрезать корпоративный ландшафт на сервисы вокруг бизнес-возможностей, а не вокруг отделов или legacy-систем.

Контекст рождения — большая enterprise. Десятки разрозненных систем, у каждой свой формат данных и протокол. SOA отвечала на вопрос «как их интегрировать», а не «как нарезать одно приложение». Это разница в фокусе, и она объясняет почти всё остальное.

ESB как центр мира

Каноническая SOA почти всегда крутится вокруг ESB (Enterprise Service Bus) — центральной шины интеграции. Сервисы не зовут друг друга напрямую, всё идёт через шину.

Что делает ESB: - routing — куда доставить сообщение, решает шина, а не отправитель. - transformation — на лету переводит формат одного сервиса в формат другого. - protocol bridging — один говорит по SOAP, другой по JMS, шина их мирит. - orchestration — связывает несколько сервисов в один бизнес-процесс, часто на BPEL.

        ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
        │ Billing  │   │  CRM     │   │ Inventory│   │ Legacy   │
        │ service  │   │ service  │   │ service  │   │ mainframe│
        └────┬─────┘   └────┬─────┘   └────┬─────┘   └────┬─────┘
             │              │              │              │
             ▼              ▼              ▼              ▼
        ════════════════════════════════════════════════════════
        ║         ESB (routing, transform, orchestration)       ║
        ════════════════════════════════════════════════════════
             ▲              ▲              ▲
             │              │              │
        ┌────┴─────┐   ┌────┴─────┐   ┌────┴─────┐
        │ Orders   │   │ Shipping │   │ Partner  │
        │ service  │   │ service  │   │ gateway  │
        └──────────┘   └──────────┘   └──────────┘

Вся логика интеграции стянута в шину. Это и есть smart pipes — труба умная, концы тупые. Отсюда главная грабля канонической SOA: ESB становится узким местом и точкой централизованного контроля. Команда шины упирается в очередь из всех остальных команд.

Технологический стек канона

SOA конца 90х и нулевых почти срослась с конкретным стеком, хотя сама идея от него не зависит.

  • SOAP — XML-протокол вызова сервисов поверх HTTP или очередей.
  • WSDL — машинно-читаемое описание контракта сервиса.
  • WS-* — семейство стандартов: WS-Security, WS-ReliableMessaging, WS-AtomicTransaction и прочие.
  • UDDI — реестр сервисов, чтобы их находить. На практике почти не взлетел.

Note

Главная путаница вокруг SOA — приравнять её к этому стеку. SOA это философия дизайна, независимая от протокола. SOAP/WS-*/ESB — одна (и часто ругаемая) реализация, не определение. REST-сервисы вокруг бизнес-возможностей — тоже SOA.


Чем отличается от Microservices

Микросервисы и SOA — родственники, граница местами размытая. Но канонически расходятся по нескольким осям.

Ось SOA (канон) Microservices
Размер сервиса крупные, вокруг бизнес-домена мелкие, вокруг одной capability
Коммуникация через ESB, smart pipes dumb pipes, smart endpoints
Данные часто общая БД / общая модель у каждого сервиса своя БД
Координация централизованная orchestration в ESB choreography или лёгкая orchestration
Управление governance сверху, единые стандарты автономия команд
Протоколы SOAP/WS-*, тяжёлые контракты HTTP/REST, gRPC, лёгкие контракты

Ключевое расхождение — где живёт интеллект. В SOA логика интеграции в трубе (ESB). В микросервисах труба тупая, просто доставляет байты, а ум в endpoint'ах. Это smart endpoints, dumb pipes — формулировка из канонической статьи Lewis/Fowler (см. микросервисы).

Второе расхождение — данные. Канон SOA терпит общую БД и общую модель на несколько сервисов. Микросервисы это запрещают: своя БД у каждого, иначе получаешь скрытый coupling через схему. Подробнее про smart vs dumb pipes и стили коммуникации — в Communication & API Style.

«SOA done right» — спор о наследовании

Микросервисы часто называют «SOA done right» или fine-grained SOA. Логика такая: идея сервисов вокруг бизнес-возможностей была правильной, испортила её централизация — тяжёлый ESB, governance сверху, общая БД. Микросервисы взяли ту же идею и выкинули центр.

Спор реальный, у обеих сторон есть резон: - За: цель одна — декомпозиция по бизнес-возможностям, сетевые контракты, переиспользование. Микросервисы это SOA, доведённая до автономных команд и децентрализованных данных. - Против: расхождение принципиальное, а не косметическое. Decentralized data и dumb pipes — не «улучшение SOA», а отказ от её центральной механики (шины и общей модели). Разные звери.

Практический вывод от спора не зависит. Строишь сервисы вокруг бизнес-доменов и не тащишь центральную шину с общей БД — называй как хочешь, важно избегать known граблей канона.


Плюсы

  • Интеграция гетерогенного зоопарка — главный исторический смысл. Десять систем на разных технологиях начинают говорить через единые контракты.
  • Переиспользование сервисов между приложениями. Один CustomerService обслуживает и биллинг, и CRM, и портал.
  • Централизованный governance — единые стандарты безопасности, логирования, мониторинга, навязанные сверху. В большой регулируемой организации это плюс, а не только минус.
  • Бизнес-ориентированная декомпозиция как явный принцип — задолго до DDD в мейнстриме.

Минусы

  • ESB как single point of failure и узкое место — техническое и организационное сразу. Падает шина — встаёт всё.
  • Smart pipes прячут логику в инфраструктуре. Поведение размазано между сервисами и шиной, отлаживать тяжело.
  • Общая БД / общая модель — скрытый coupling. Изменил схему ради одного сервиса, сломал три других.
  • Тяжёлый стек: SOAP/WS-*/BPEL многословны, инструменты громоздкие, порог входа высокий.
  • Vendor lock-in. ESB-продукты дорогие и проприетарные, governance часто скатывается в бюрократию.

Когда подходит

  • Большой enterprise с зоопарком legacy-систем, которые надо интегрировать, а не переписывать.
  • Жёсткие требования к централизованному governance — регуляторика, единый контроль, аудит.
  • Есть выделенная интеграционная команда, готовая держать шину и стандарты.
  • Зелёный проект с сервисами вокруг доменов — обычно смотришь в сторону микросервисов или модульного монолита, а не канонической ESB-SOA.

Microservices

Microservices — это система из независимо разворачиваемых сервисов, каждый построен вокруг своей бизнес-области, со своим process/runtime и обычно своей БД, общающихся по сети.

Термин зафиксировали James Lewis и Martin Fowler — на воркшопе архитекторов под Венецией в мае 2011, устаканили к маю 2012, каноническое определение вышло статьёй в марте 2014. Практику параллельно тащили Adrian Cockcroft (Netflix), Fred George, Daniel Terhorst-North. Книга-ориентир — Sam Newman, "Building Microservices".

Главная мысль не про "micro". Размер сервиса — следствие, не цель. Канон давит на три вещи:

  • independent deployability — сервис катишь в прод отдельно, не пересобирая остальную систему
  • decentralized data — у каждого сервиса свой store, лезть в чужую БД напрямую нельзя
  • организация вокруг бизнес-capability, а не вокруг технических слоёв
                         ┌─────────────┐
            клиенты ───► │ api-gateway │
                         └──────┬──────┘
              ┌─────────────────┼─────────────────┐
              ▼                 ▼                 ▼
        ┌──────────┐      ┌──────────┐      ┌──────────┐
        │  orders  │      │ payments │      │ shipping │
        └────┬─────┘      └────┬─────┘      └────┬─────┘
             ▼                 ▼                 ▼
        ┌──────────┐      ┌──────────┐      ┌──────────┐
        │ orders_db│      │ pay_db   │      │ ship_db  │
        └──────────┘      └──────────┘      └──────────┘

        sync: REST / gRPC по запросу     async: events через broker

api-gateway — единая точка входа: routing, auth, rate limiting, иногда композиция ответа. Между собой сервисы говорят либо синхронно (REST/gRPC, request/response), либо асинхронно (events через broker).

Главная цена: это распределённая система

Самое важное про микросервисы — не что они дают, а чем за это платишь. Монолитный вызов функции превращается в сетевой. И вот что прилетает следом:

  • сеть ненадёжна — таймауты, retries, потерянные пакеты. Любой вызов может не дойти или дойти дважды
  • partial failure — один сервис лежит, остальные живут. Систему проектируешь так, чтобы она деградировала, а не падала целиком
  • eventual consistency — данные размазаны по разным БД. Глобального согласованного снимка нет, читаешь почти всегда чуть устаревшее
  • распределённые транзакции — ACID на несколько сервисов не натянешь. Вместо них saga и outbox, то есть compensation вместо rollback и согласованность по факту
  • отладка и observability — стек-трейс не пересекает границу сервиса. Нужны distributed tracing, корреляционные id, агрегированные логи, иначе "почему упало" не ответить
  • операционные расходы — деплой, сеть, секреты, мониторинг на N сервисов вместо одного процесса

Грабли: - порезал на сервисы, но они деплоятся только вместе — это distributed monolith. Вся цена распределённой системы, ни одного плюса. Провал не в числе сервисов, а в coupling: общие контракты, общая БД, синхронные цепочки вызовов - "micro" понял буквально и нарезал сотню крошечных сервисов — получил сетевой ад вместо модульности

Когда окупается

Цена высокая, поэтому платят её не всегда. Грубо говоря, окупается, когда:

  • много команд — каждая владеет своим сервисом и катит его независимо, не упираясь в общий релизный поезд
  • нужен независимый scale — горячий компонент масштабируешь отдельно, а не тянешь весь монолит
  • разные требования у компонентов — одному нужен другой язык/runtime/store, и это оправдано
  • независимый deployment как самоцель — выкатывать часть системы, не трогая остальное

Когда не подходит: - маленькая команда и непонятные границы домена — сначала монолит, режешь на сервисы потом, когда bounded contexts проявились. Это и есть MonolithFirst (Fowler, 2015) - early-stage продукт, где границы ещё едут каждую неделю — распределённые границы переделывать дорого

Note

Conway's Law (Conway, 1968): структура системы повторяет структуру коммуникаций в организации. Поэтому "сервис на команду" работает — границы сервисов ложатся на границы команд. Если команда по сути одна, а сервисов десять, ты воюешь с законом Конвея.

Канон vs индустрия: - канон (Lewis/Fowler) делает акцент на independent deployability и decentralized data, а не на размере - индустрия часто читает "micro" как "маленький" и плодит сервисы ради числа — это и приводит к distributed monolith

Как резать систему на сервисы по bounded contexts и эксплуатировать всё это — saga, outbox, distributed tracing, observability — отдельная большая тема: Микросервисы.


Self-Contained Systems (SCS)

Self-Contained System — это автономная веб-система-вертикаль, которая включает свой UI, бизнес-логику и данные и владеет своим куском функциональности end-to-end. - придумали в INNOQ, публично оформили на scs-architecture.org примерно в 2015, главный рупор — Stefan Tilkov. - ключевое слово — вертикаль. Не слой, не сервис под чужим фронтом. Целый кусок продукта от пикселя в браузере до строки в своей БД. - SCS почти не вызывает чужую логику синхронно в рантайме. Если интеграция и есть — то через ссылки, UI-композицию или async. Это главное, что отличает SCS от привычного service mesh.

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


Принципы

Канонический список с scs-architecture.org, без воды: - автономия — каждый SCS отдельное веб-приложение, деплоится независимо, своя кодовая база. - своя БД — никакого shared database между SCS. Данные дублируются, а не шарятся. - своя логика и свой UI — UI не отдельный фронт сверху, он внутри SCS. Часть вертикали. - no shared business code — общий бизнес-код между SCS под запретом. Шаблоны, утилиты — можно, доменную логику — нет, иначе получишь скрытый coupling. - async-интеграция по умолчанию — между SCS предпочтительно async или UI-композиция, sync-вызов в рантайме — крайняя мера. - свой API опционален — SCS может вообще не иметь публичного API, если общается через UI и события. - своя команда — один SCS принадлежит одной команде. Conway's Law не борешь, а используешь.

Note

Формулировки и количество принципов на сайте со временем правили. Держись смысла, а не точного числа: автономия деплоя, своя БД, UI внутри, минимум синхронной связи.


Чем отличается от микросервисов

Самая частая ошибка — считать SCS «микросервисом с UI». Разница в зерне и в способе связи.

Признак Microservices SCS
Зерно мелкое, много сервисов крупное, единицы систем
UI обычно отдельный фронт UI внутри, обязателен
Связь в рантайме часто sync RPC/HTTP async или UI-композиция, sync избегают
Сколько на продукт десятки-сотни единицы (грубо 3-25)
Граница технический сервис бизнес-вертикаль end-to-end

Чем отличается: - SCS крупнее. Это не один эндпоинт, а целая автономная система. Внутри одного SCS может жить несколько микросервисов — масштаб другой. - UI обязателен. У микросервиса UI обычно где-то снаружи, у SCS он часть вертикали. - связь другая. Микросервисы спокойно ходят друг к другу sync в рантайме. SCS это осознанно не делает — иначе собрал distributed monolith (Communication & API Style), просто покрупнее.

Канон vs индустрия: - в индустрии SCS часто зовут «вариантом микросервисов» или валят в одну кучу. На уровне маркетинга ок, по сути — другое зерно и другая модель интеграции. - больше про сам microservices-стиль — в микросервисы. SCS можно читать как ответ на «микросервисы расплодились и связались синхронно намертво».


Топология

Каждый SCS — полная вертикаль. UI, логика, БД внутри. Снаружи — общий вход и тонкая интеграция.

              browser
                 |
        +--------v---------+
        |  routing / shell |   тонкий слой: маршрутизация + UI-композиция
        +--+----+------+---+
           |    |      |
   +-------v+  +v------+  +v--------+
   |  SCS A |  | SCS B |  |  SCS C  |    каждый — отдельная команда
   |--------|  |-------|  |---------|
   |  UI    |  |  UI   |  |  UI     |    UI внутри вертикали
   |  logic |  | logic |  | logic   |
   |  DB    |  |  DB   |  |  DB     |    своя БД, без shared
   +---+----+  +---+---+  +----+----+
       |           |           |
       +-----------+-----------+
                   |
            async events / broker        интеграция между SCS — async,
                                         без sync-вызова чужой логики

Что важно на схеме: - shell сверху тонкий. Он маршрутизирует и склеивает UI-фрагменты, но не держит бизнес-логику. - стрелки между SCS идут через broker, не напрямую sync. Падение SCS B не валит SCS A. - БД нарисованы внутри вертикалей не случайно — пересечения данных между ними нет.

UI-композиция — отдельная тема. Склеивать фронты можно по-разному: - ссылки — самый дешёвый вариант. SCS A просто линкует на страницу SCS B. Ноль связности. - server-side includes / Edge Side Includes — shell собирает страницу из HTML-фрагментов разных SCS. - client-side композиция — фрагменты тянутся в браузере (web components, micro-frontends).


Плюсы

  • автономия деплоя реальная, а не на словах. Своя БД и no shared code убирают скрытый coupling, из-за которого микросервисы часто выкатываются пачкой.
  • отказоустойчивость по умолчанию. Нет sync-цепочек в рантайме — нет каскадных падений. Упал один SCS, остальные живут, максимум пропадёт фрагмент на странице.
  • чистая граница владения. Один SCS — одна команда. Меньше кросс-командных согласований.
  • технологическая свобода. Каждый SCS можно писать на своём стеке, раз нет общего кода и общей БД.

Минусы

  • дублирование данных. Раз shared database нет, одни и те же данные лежат в нескольких SCS и синкаются через события. Eventual consistency и весь её хвост — твоя забота.
  • UI-композиция — нетривиальная инженерия. Склеить фронты от разных команд в один цельный продукт без визуальных швов сложнее, чем кажется.
  • крупное зерно режется редко. Нарезать продукт на правильные вертикали трудно. Ошибся с границей — переезд дорогой, дороже чем у мелкого сервиса.
  • сквозной UX. Единый стиль, общая навигация, сквозная авторизация между автономными SCS требуют отдельной дисциплины и общих соглашений.

Когда подходит

  • большой продукт с явными бизнес-областями, которые режутся на вертикали (портал, e-commerce, кабинет с разными доменами).
  • несколько команд, и хочешь дать каждой полную автономию от UI до БД.
  • интеграция между частями терпит async и eventual consistency — нет жёсткого требования «здесь и сейчас синхронно».
  • хочешь плюсы микросервисной автономии, но боишься расплодить сотни мелких сервисов и связать их sync-вызовами намертво.

Когда не стоит

  • маленькое приложение или одна команда — overhead вертикалей не окупится, начни с модульного монолита.
  • предметка плохо режется на UI-вертикали (например, чистый backend без своего фронта — там SCS по определению не про это).
  • между частями нужна сильная синхронная консистентность в рантайме — async-модель SCS будет мешать, а не помогать.

Backend-for-Frontend (BFF)

Backend-for-Frontend — это отдельный server-side слой под один конкретный фронтенд, который агрегирует и подгоняет данные downstream-сервисов под нужды именно этого UI.

  • один UI — один BFF: iOS-BFF, Android-BFF, web-BFF, и каждым владеет своя UI-команда.
  • BFF тонкий: собирает ответы нескольких сервисов в одну форму, переводит протоколы, режет лишние поля. Domain logic тут не живёт.
  • паттерн родом из SoundCloud, описал Phil Calçado в 2015 (philcalcado.com). Идею внутри команды он приписывает коллеге Lukasz Plotnicki. Часто всплывающий «Nick Fisher» — атрибуция сомнительная.

Sam Newman потом разнёс паттерн по индустрии в статьях и книгах про микросервисы.


Зачем вообще понадобился

Началось с боли общего API. Один backend на всех клиентов — вечный компромисс.

  • мобайл хочет мало: узкий канал, дорогой трафик, маленький экран. Ему нужно три поля, а общий endpoint отдаёт двадцать.
  • web хочет много: больше данных за раз, другие экраны, другая навигация.
  • under-fetching и over-fetching одновременно: один клиент недополучает, другой тащит лишнее.
  • разные клиенты эволюционируют с разной скоростью. Общий API становится узким местом, где сталкиваются все команды сразу.

Что это значит: - каждая UI-команда правит общий контракт под себя. Свалка компромиссов, где никому не удобно. - любое изменение под мобайл рискует задеть web и наоборот. Координация дорожает с числом клиентов.

Идея простая: - дай каждому клиенту свой backend. Мобильная команда оптимизирует свой endpoint под мобайл, web-команда — под web. Никто никому не мешает.


Топология

        iOS app            Android app            Web SPA
           |                    |                    |
      +----------+         +----------+         +----------+
      | iOS BFF  |         | Android  |         | Web BFF  |
      | (thin)   |         |   BFF    |         | (thin)   |
      +----------+         +----------+         +----------+
           |     \            /    \            /     |
           |      \          /      \          /      |
           v       v        v        v        v       v
      +---------+  +-----------+  +-----------+  +-----------+
      | Orders  |  |  Catalog  |  |  Users    |  |  Pricing  |   downstream services
      +---------+  +-----------+  +-----------+  +-----------+
  • BFF сидит между клиентом и пачкой сервисов. Один клиент ходит только в свой BFF.
  • BFF делает fan-out по нужным сервисам, склеивает ответы, отдаёт ровно ту форму, что нужна этому экрану.
  • downstream-сервисы общие. BFF их не дублирует, а композирует.

Граница владения тут не техническая, а организационная — чистый Conway's Law (см. Communication & API Style). BFF принадлежит той же команде, что пишет клиент. Контракт между клиентом и BFF меняется внутри одной команды, без межкомандной координации.


Чем отличается от API Gateway

Их постоянно путают: оба сидят на входе, оба фронтят сервисы. Разница в количестве и в назначении.

Свойство API Gateway BFF
Сколько один общий вход на всех по одному на каждый UX
Кому служит всем клиентам сразу конкретному клиенту
Что делает routing, auth, rate limit, единый вход агрегация и подгонка под один UI
Кто владеет платформенная/infra команда UI-команда этого клиента
Форма ответа более-менее generic заточена под экраны клиента
  • API Gateway (см. микросервисы) — это общая дверь. Cross-cutting concerns в одном месте: auth, rate limiting, protocol translation, маршрутизация.
  • BFF — это по двери на клиента, и каждая дверь знает свой UI в лицо.
  • одно другому не мешает. Часто BFF стоят за общим Gateway: Gateway держит auth и rate limit на периметре, дальше трафик расходится по BFF. Gateway — горизонталь, BFF — вертикали под ним.

Плюсы и минусы

Плюсы: - UI-команда владеет своим backend целиком. Меняешь экран — меняешь свой BFF, никого не спрашивая. - нет общего компромиссного API. Каждый клиент получает ровно свою форму данных, без over/under-fetch. - BFF деплоится и катится независимо от других BFF. Релизный цикл клиента не завязан на чужие. - downstream-сервисы остаются чистыми и generic — вся клиент-специфика уезжает в BFF.

Минусы: - дублирование. Похожая агрегация и трансляция переписываются в каждом BFF. Web и Android часто хотят почти то же самое — и пишут это дважды. - N клиентов — N backend-ов, которые надо писать, деплоить, мониторить, дежурить. - соблазн утащить domain logic в BFF. Чуть зазевался — и бизнес-правила расползлись по слоям-фасадам, где их быть не должно. - общий код между BFF — отдельная головная боль. Выносишь в библиотеку — связываешь команды обратно. Не выносишь — копипаста. Золотой середины нет, выбираешь яд по контексту.

Грабли: - BFF — не место для бизнес-логики. Это тонкий слой агрегации и перевода. Как только в нём появляются решения «можно ли», а не «как показать» — это уже не BFF, а скрытый сервис. - не делай один общий BFF на всех. Это снова API Gateway под другим именем, и вся затея теряет смысл.

Когда подходит: - несколько разных клиентов с реально разными нуждами (мобайл vs web vs партнёрский API). - у каждого клиента своя команда, и хочется развязать их релизы. - общий API уже стал узким местом, где дерутся за поля.

Когда перебор: - один клиент, один web. Тогда BFF — это просто твой backend, отдельное имя ему не нужно. - клиенты почти одинаковые. Дублирование съест выгоду, проще жить с общим API.


Event-Driven Architecture (EDA), CQRS, Event Sourcing

Семейство стилей вокруг одной идеи: общаться не вызовами, а фактами о произошедшем. EDA — про интеграцию компонентов. CQRS — про разделение модели чтения и записи. Event Sourcing — про то, что источник истины это поток событий. Их часто валят в кучу, хотя они независимы.


Event-Driven Architecture (EDA)

Event-Driven Architecture — это стиль, где компоненты общаются, производя события и реагируя на них, а не дёргая друг друга напрямую через request/response.

Термин раскрутили аналитики Gartner (в первую очередь Roy Schulte) примерно в 2003-м. Корни глубже — publish/subscribe и message-driven системы 80-90х. Отдельной seminal-статьи, которая «придумала» EDA, нет. Идея кодифицировалась постепенно, в том числе в Enterprise Integration Patterns (Hohpe & Woolf, 2003).

Идея простая: - producer публикует event («заказ оплачен») в broker и забывает про него. - producer не знает, кто его слушает и слушает ли вообще. Ноль связи с consumer-ами. - consumer-ы подписаны на нужные события и реагируют каждый по-своему, async.

Broker посередине — Kafka, RabbitMQ, NATS. Он принимает события и раздаёт подписчикам.

                                          +----------------------+
                                     +--> | consumer: биллинг    |
                                     |    +----------------------+
+------------+   order.paid   +------+--+  +----------------------+
| producer   | -------------> | broker  |--> | consumer: склад    |
| (checkout) |                | (Kafka) |    +----------------------+
+------------+                +------+--+  +----------------------+
                                     +--> | consumer: аналитика  |
                                          +----------------------+
       producer не знает про этих троих. Добавить четвёртого — producer не трогаешь.

Здесь и видна низкая связность: новый consumer подключаешь без изменений в producer. Это главный аргумент за EDA.

Плюсы: - decoupling — producer и consumer не знают друг о друге, развиваются независимо. - scale — consumer-ов масштабируешь отдельно, под свою нагрузку. - буфер нагрузки — broker гасит пики, consumer разгребает очередь в своём темпе (backpressure). - расширяемость — новый потребитель события подключается без касания источника.

Минусы: - eventual consistency — состояние сходится не мгновенно. С этим живёшь, а не борешься. - отладка потоков — нет одного стектрейса. Путь события размазан по сервисам, нужен distributed tracing и correlation id. - event spaghetti — когда событий много и кто на что реагирует неочевидно, система превращается в неявный граф зависимостей. Хуже монолита, потому что связи не видно в коде. - дублирование и идемпотентность — broker обычно даёт at-least-once delivery. Одно событие может прийти дважды. Consumer обязан быть идемпотентным (детали — outbox, dedup — в Микросервисы).

Грабли: - «thin» event (только id и тип, «заказ X оплачен») заставляет consumer-а идти назад в producer за деталями — снова появляется связь. «Fat» event (event-carried state transfer) тащит данные внутри, но раздувается и дублирует состояние. Выбор — это компромисс, не бесплатно.


Choreography vs orchestration

Два способа организовать поток из нескольких шагов.

  • choreography — нет дирижёра. Каждый сервис слушает события и сам решает, что делать дальше, публикуя свои. Поток «складывается» из реакций. Максимальный decoupling, но общую картину процесса нигде не видно — она живёт в головах и в логах.
  • orchestration — есть явный координатор (orchestrator). Он знает шаги процесса и по очереди дёргает участников. Поток виден в одном месте, отлаживать проще, но координатор — точка связности и узкое место.

Грубо говоря: choreography хороша для простых независимых реакций, orchestration — когда процесс сложный и важно видеть его целиком. На практике в одной системе встречаются оба.


CQRS

CQRS (Command Query Responsibility Segregation) — это разделение модели на две: одна обрабатывает команды (меняют состояние), другая — запросы (читают состояние).

Термин ввёл Greg Young около 2010-го, поверх принципа Command-Query Separation Бертрана Мейера (CQS, «Object-Oriented Software Construction», 1988). У Мейера CQS про методы: метод либо меняет состояние, либо возвращает данные, но не одновременно. Young поднял идею на уровень модели целиком. Термин раскрутила bliki-статья Мартина Фаулера «CQRS» (2011).

Зачем: - read и write нагружаются по-разному. Запросов обычно на порядки больше, чем команд. - разные модели хотят разной формы данных: write — нормализованная под инварианты, read — денормализованная под конкретные экраны. - разделив пути, каждый масштабируешь и оптимизируешь независимо.

На макро-уровне это отдельные read/write пути, часто с отдельными хранилищами: пишешь в одну базу, читаешь из другой (read model), которую обновляешь асинхронно из событий.

команда         +------------+      событие      +------------+      запрос
--------------> | write      | ----------------> | read model | <--------------
                | model + DB |                   | (проекция) |
                +------------+                    +------------+
        write-путь: инварианты,            read-путь: денормализовано,
        нормализация, консистентность      быстро, под конкретные view

Канон vs индустрия: - частое искажение — что CQRS обязывает иметь две базы плюс event sourcing плюс async messaging. И Young, и Фаулер прямо предупреждают: это разделение на уровне модели, оно может быть in-process, в одном процессе и одной базе. - переусложнение — CQRS без реальной нужды добавляет сложность, а не убирает.

Когда это макро-решение: - если у тебя реально разные пути и хранилища для чтения и записи, разнесённые по топологии — это влияет на всю систему, и тогда CQRS уместен на уровне system design. - если разделение живёт внутри одного сервиса как способ организовать код — это локальная деталь, не макро-архитектура. Не тащи её в обсуждение топологии.


Event Sourcing

Event Sourcing — это хранение полной последовательности событий как источника истины, где текущее состояние выводится повтором (replay) этих событий, а не лежит готовым.

Обычная база хранит последнее состояние: баланс 100. Event Sourcing хранит историю: +50, +70, -20. Баланс = свёртка событий. Состояние здесь — проекция, производная от лога.

Канонические фигуры — Мартин Фаулер (назвал и описал паттерн в bliki, декабрь 2005) и Greg Young (довёл до практики в DDD/CQRS-сообществе, ~2006-2010).

Зачем: - полная история «из коробки» — аудит, кто и когда что изменил, бесплатно. - можно восстановить состояние на любой момент в прошлом (time travel). - из одного лога строишь сколько угодно read-проекций под разные нужды.

Грабли: - versioning событий — схема события меняется со временем, а старые в логе остаются. Эволюцию схемы закладываешь сразу, иначе replay сломается. - лог событий — это не обычная audit-таблица, которую можно править. Событие иммутабельно: оно уже произошло. Мутировать его нельзя. - цена replay — на большом логе восстановление состояния дорого, спасают snapshot-ы.

Канон vs индустрия: - Event Sourcing и CQRS постоянно путают и считают одним. Они независимы: CQRS бывает без ES, ES бывает без CQRS. Вместе встречаются часто, но это не обязательная связка. - паттерн редкий и сложный. Берёшь его осознанно, под аудит и темпоральные требования, а не по умолчанию.


Saga — транзакции через события

Saga — это длинная транзакция как последовательность локальных транзакций, где у каждого шага есть компенсирующая операция, откатывающая ранее сделанное, если поздний шаг упал.

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

Происхождение: Hector Garcia-Molina и Kenneth Salem, «Sagas», SIGMOD '87. Изначально — про длинные транзакции в одной БД, не про микросервисы. Привязка к распределённым системам — поздняя переинтерпретация сообществом.

Что это значит: - saga даёт eventual consistency через компенсации, не атомарность и не изоляцию. - компенсация — это не «откат» как в БД. Это новое действие, семантически отменяющее прежнее (вернуть деньги, отменить бронь). Промежуточные состояния видны.

Две формы прямо ложатся на choreography vs orchestration: - choreography-saga — шаги связаны через события, дирижёра нет. - orchestration-saga — отдельный saga-orchestrator ведёт процесс и вызывает компенсации.

Цена: - идемпотентность и дедупликация обязательны — компенсации и повторы при at-least-once должны быть безопасны при повторном применении. - реализация на уровне сообщений (outbox pattern, гарантия доставки, idempotency) — детали в Микросервисы.


Serverless / FaaS

Serverless / FaaS — это модель, где код выполняется как короткоживущие event-triggered функции на инфраструктуре, которую полностью предоставляет и масштабирует провайдер, с оплатой за исполнение и без управления серверами со стороны разработчика.

Точка отсчёта по индустрии — запуск AWS Lambda, ноябрь 2014. Сам стиль разобрали Mike Roberts и John Chapin в «Serverless Architectures» (martinfowler.com, 2016/2018). Термин «serverless» шире FaaS и включает managed-сервисы (BaaS): очереди, хранилища, аутентификацию как сервис.

Что это значит

  • функция, а не процесс. Единица деплоя — отдельная функция-обработчик, не постоянно живущий сервис.
  • запускается событием. HTTP-запрос, сообщение в очереди, файл в хранилище, cron — триггер поднимает функцию, она отрабатывает и умирает.
  • масштабирование на провайдере. Пришла тысяча событий — провайдер поднял тысячу инстансов, вплоть до scale-to-zero, когда трафика нет. Платишь за исполнение, не за простой.
  • stateless по построению. Между вызовами состояния нет — всё, что надо сохранить, уезжает во внешний store (БД, Redis, объектное хранилище).
   событие                  поднимается           масштабирует
   (HTTP / queue / cron) ──► [ function ] ◄─────── провайдер (0..N инстансов)
                          внешний state (БД, Redis, S3)

Плюсы

  • нет управления серверами — ни патчей, ни capacity planning, no-ops в пределе.
  • авто-scale из коробки, включая scale-to-zero. На спорадической нагрузке платишь почти ноль.
  • быстрый старт фичи — выкатил функцию, не поднимая инфраструктуру под неё.

Минусы и грабли

  • cold start. Простаивавшая функция поднимается с задержкой — для latency-критичного пути это больно.
  • лимиты. Время исполнения, память, размер пакета ограничены провайдером. Долгие задачи не влезают.
  • vendor lock-in. Триггеры, формат, окружение завязаны на провайдера. Переезд между облаками дорогой.
  • stateless и эфемерность. Состояние держать негде — только во внешних сервисах, и это часть дизайна.
  • отладка и локальный запуск сложнее обычного процесса — distributed-картина, эмуляция окружения.

Note

«Serverless» не значит «серверов нет». Серверы есть — их прячет и обслуживает провайдер. И функции не живут постоянно: они эфемерны и поднимаются по событию, отсюда cold start.

Когда подходит

  • спорадическая, событийная нагрузка — загрузка файлов, webhooks, cron-задачи, редкие пики.
  • glue-логика между сервисами — небольшие обработчики, которые держать как сервис накладно.
  • не для постоянного low-latency hot path — там обычный сервис обычно дешевле и предсказуемее.

Actor-Model Cluster и Pipes & Filters / Data-Pipeline

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

Actor-Model Cluster

Actor model — это модель конкурентных вычислений, где actor — единственный примитив: в ответ на сообщение он шлёт сообщения другим, создаёт новых акторов и меняет своё поведение для следующего сообщения. Carl Hewitt, Peter Bishop, Richard Steiger, IJCAI 1973.

Никакого shared mutable state. Каждый actor — это: - своё приватное состояние, до которого никто не дотянется напрямую; - mailbox — очередь входящих сообщений; - однопоточная обработка: одно сообщение за раз, поэтому внутри actor локи не нужны.

Конкурентность тут не через locks вокруг общей памяти, а через изоляцию. Состояние спрятано, общение — только асинхронными сообщениями. Гонок за память нет по построению.

Cluster добавляет два свойства поверх модели: - location transparency — шлёшь сообщение по логическому адресу, не зная, на какой ноде сидит получатель. Локальный он или удалённый — код одинаковый. - supervision — акторы выстроены в дерево. Падает дочерний — родитель-supervisor решает, что делать: рестартнуть, поднять поддерево, эскалировать выше. Философия «let it crash»: не обмазываешь каждый вызов try/catch, а даёшь упасть и перезапускаешь в чистом состоянии.

Канон подхода — Erlang/OTP, выросший в телекоме Ericsson, где аптайм был требованием, а не лозунгом. На JVM ту же модель принёс Akka. Из свежего — Microsoft Orleans с virtual actors.

Зачем: - высокая конкурентность — миллионы лёгких акторов на процесс, дешевле OS-потоков на порядки; - stateful по природе — состояние живёт в акторе в памяти, не гоняешь его в базу на каждый чих; - отказоустойчивость через supervision — падение одного актора обычно не валит систему, дерево локализует сбой.

Когда подходит: - телеком, биллинг в реалтайме, игровые серверы, IoT-шлюзы, трейдинг — много мелких независимых сущностей с собственным состоянием и долгим временем жизни; - нужна отказоустойчивость и горячий рестарт части системы без полной остановки; - модель «сущность = actor» ложится естественно: один пользователь, одна сессия, одно устройство — один actor.

        supervisor (root)
         /      |      \
   worker-A  worker-B  worker-C        <- дерево supervision
      |
   [mailbox] <- msg <- msg <- msg      <- по одному сообщению за раз

   нода 1                 нода 2
  [actor X]  --msg-->    [actor Y]     <- location transparency:
                                          адрес логический, сеть скрыта

Грабли: - модель не даёт гарантий доставки и порядка из коробки. Сообщения асинхронные, глобального порядка нет, доставка обычно at-most-once или at-least-once — не exactly-once. - location transparency прячет сеть, но не отменяет её. Удалённый actor — это сетевой вызов со своими таймаутами, разрывами и partition. Прозрачность адреса не делает сеть надёжной. - distributed transactions поверх акторов сами собой не появляются. Согласованность между акторами — это saga и компенсации (см. Communication & API Style), а не магия фреймворка.

Чем отличается: - от обычного thread-pool — actor инкапсулирует состояние, его не надо защищать локами; единица не «задача в пуле», а долгоживущая сущность с mailbox. - от микросервисов — actor это примитив внутри процесса или кластера, не deployable unit. Гранулярность на порядки мельче, общение — нативные сообщения, не HTTP/gRPC через границу деплоя.


Pipes & Filters / Data-Pipeline

Pipes and Filters — это архитектура, где поток данных проходит через серию независимых filter-компонентов: каждый трансформирует вход и отдаёт результат через pipe следующему. Корни — Unix pipes (Douglas McIlroy, Bell Labs, реализация в Version 3 Unix, 1973). Как паттерн формализован в POSA Vol. 1 (Buschmann et al., 1996).

Две сущности, и всё: - filter — одна трансформация над данными. В идеале stateless: получил порцию, преобразовал, отдал дальше. Про соседей по конвейеру ничего не знает. - pipe — канал между фильтрами. Переносит данные и развязывает фильтры по времени: каждый работает в своём темпе.

Идея простая: разбиваешь сложную обработку на маленькие шаги, соединяешь их трубами. Классика — cat access.log | grep 500 | awk '{print $7}' | sort | uniq -c. Каждая утилита — filter, | — pipe. Та же топология лежит в основе ETL и стриминга: Spark, Flink, Beam — это Pipes & Filters, разнесённые по кластеру и обмазанные parallelism, backpressure и fault tolerance.

Зачем: - composability — фильтры собираются в разные конвейеры как кубики. Добавить стадию, переставить, выкинуть — не трогая остальные. - переиспользование — один filter (парсинг, дедуп, фильтрация) живёт во многих пайплайнах. - параллелизм почти даром — стадии бегут одновременно на разных порциях, как ступени конвейера. Раскидать filter по нодам тоже несложно: общего состояния нет.

Когда подходит: - обработка потоков или батчей данных: ETL, агрегация логов, индексация, ML feature pipelines, трансформация событий; - задача естественно ложится в линейные стадии «получил → преобразовал → отдал»; - стадии хочется масштабировать и тасовать независимо.

  source --pipe--> [filter:parse] --pipe--> [filter:filter] --pipe--> [filter:aggregate] --> sink

  один filter:   in --> ( трансформация ) --> out      <- stateless, про соседей не знает

  параллелизм:        --> [parse #1] -->
  source --split-->   --> [parse #2] -->  --merge--> [aggregate] --> sink
                      --> [parse #3] -->

Грабли: - паттерн не обязан быть последовательным, батчевым и одномашинным. Это частый стереотип из-за Unix-корней. Современные пайплайны распределены и стримят в реалтайме. - сила — в композиции stateless-фильтров. Как только filter обрастает состоянием (оконные агрегации, джоины), всплывают concerns, которых в Unix-формулировке не было: parallelism, backpressure, fault tolerance, exactly-once. Дёшево это уже не даётся. - «data pipeline» в названии инструмента не равно этому паттерну. Не каждый ETL-тул — чистые Pipes & Filters, и наоборот.

Чем отличается: - от EDA (см. Communication & API Style) — тут data flow по фиксированной топологии стадий, а не реакция компонентов на события. Pipe соединяет два конкретных фильтра, а не вещает событие в эфир. - от микросервисов — filter это стадия обработки, а не бизнес-сервис вокруг bounded context. Деление по трансформациям данных, не по бизнес-возможностям (см. микросервисы).


Distributed Monolith — анти-паттерн

Distributed Monolith — это система, порезанная на отдельные deployable units, которые при этом завязаны так тесно, что деплоятся и меняются только вместе. - получаешь все минусы distributed system: сеть между компонентами, частичные отказы, сложный дебаг, latency на каждом вызове. - и ни одного плюса: автономии деплоя нет, независимого масштабирования нет, изоляции отказов нет. - хуже монолита и хуже честных микросервисов одновременно. Worst of both worlds.

Чёткого автора у термина нет — он всплыл в microservices-комьюнити примерно в 2014–2015 и осел в обиходе. Популяризировал его Ben Christensen (talk «Don't Build a Distributed Monolith», Microservices Practitioner Summit, январь 2016), но не он первый: в том же coupling-смысле слово встречается раньше, у Derek Gottlieb (июнь 2015). Позже разбор подхватил Sam Newman в «Monolith to Microservices» (2019).

Мысль одна: дело не в количестве сервисов, а в coupling. Можно нарезать сотню контейнеров и остаться монолитом по сути. Можно жить с тремя сервисами и быть честно распределённым.


Как выглядит

Вместо одного процесса — четыре. Но между ними синхронная цепочка, и общая база снизу.

                  ┌──────────┐   sync    ┌──────────┐   sync    ┌──────────┐
   request ─────► │ Service  │ ────────► │ Service  │ ────────► │ Service  │
                  │    A     │           │    B     │           │    C     │
                  └────┬─────┘           └────┬─────┘           └────┬─────┘
                       │                      │                      │
                       └──────────────────────┼──────────────────────┘
                                       ┌───────────────┐
                                       │  shared DB    │  ◄── одна схема на всех
                                       └───────────────┘

Запрос пользователя живёт, только пока живы все три сервиса разом. Упал C — отвалился A. Поменял таблицу под B — сломал A и C, которые читают ту же схему. Деплой одного требует деплоя остальных. Сервисов три, автономии — ноль.


Симптомы

Узнаёшь хотя бы половину — у тебя distributed monolith, как бы это ни называли в вики команды.

  • Меняешь один сервис — ломаются другие. Контракты не изолированы, изменение протекает наружу.
  • Релиз только «всё вместе». Выкатить сервис в одиночку нельзя — порядок версий жёсткий, иначе runtime падает.
  • Общая база между сервисами. Несколько сервисов пишут и читают одни таблицы напрямую, схема — общий контракт, который никто не охраняет.
  • Синхронная цепочка A -> B -> C -> D. Один запрос проходит сквозь весь стек по request/response, latency складывается, отказ любого звена рушит всю цепь.
  • Общие библиотеки с бизнес-логикой. Не утилита-хелпер, а shared-jar с доменом внутри: правишь библиотеку — пересобираешь и передеплоиваешь всех её потребителей.
  • Распределённая транзакция «на честном слове». Бизнес-операция размазана по нескольким сервисам без saga и компенсаций — консистентность держится на том, что «обычно всё проходит».

Note

Главный тест — independent deployability. Не можешь выкатить один сервис в прод, не трогая остальные, — граница ненастоящая. Всё остальное — следствия.


Как в это сползают

Почти всегда не по злому умыслу. Два типовых пути.

Порезали по техническим слоям, а не по bounded context

Классика. Взяли layered-монолит и распилили по горизонтали: сервис UI, сервис «бизнес-логика», сервис «доступ к данным». - получился three-tier, растащенный по сети. Каждый слой — отдельный deploy, но любая фича задевает все три. - одна пользовательская операция всегда идёт сквозь все слои синхронно — отсюда цепочка A -> B -> C. - граница прошла поперёк фич, а не вдоль. А резать надо вдоль bounded context (DDD, Eric Evans, 2003): одна фича — внутри одного сервиса целиком.

Bounded context — это языковая и модельная граница, не deployment unit. Один в один на сервис он не ложится. Но именно по нему проходит шов, где связи слабые, — а по техническим слоям связи, наоборот, самые плотные.

Вынесли сервисы рано, до понимания границ

Преждевременное дробление. Greenfield-проект сразу начали как «микросервисы», не нащупав, где проходят настоящие границы домена. - границы угадали неверно — данные одной фичи расползлись по двум-трём сервисам. - чтобы их сшить, появились синхронные вызовы и shared-таблицы. Coupling вернулся, но теперь через сеть. - ровно против этого Fowler написал «MonolithFirst» (2015): начинай монолитом, режь на сервисы, когда границы понятны. Не наоборот.

Note

Честно: distributed monolith — это часто результат преждевременного дробления. Не «плохо сделали микросервисы», а «сделали микросервисы слишком рано». Сначала надо было увидеть швы.


Как лечить

Лечится не разрезанием на ещё больше сервисов, а развязкой того, что уже есть. Три направления, обычно в таком порядке.

Развязать данные

Корень зла — общая база. Пока её не убрал, остальное косметика. - каждый сервис владеет своей схемой. Чужой сервис не лезет в твои таблицы напрямую — только через API или events. - начни с разделения схем внутри одной СУБД (отдельные schema/owner), потом — отдельные базы. - кросс-сервисные чтения — через реплику данных или event-carried state transfer, а не прямым JOIN в чужую таблицу.

Перевести интеграцию на async

Синхронную цепочку A -> B -> C разрываешь брокером. Вместо «вызвал и жду» — «опубликовал event, поехал дальше». - A публикует event, B и C реагируют сами. A не знает про их существование и не падает вместе с ними. - temporal coupling уходит: получатель может быть в дауне — событие подождёт в очереди. - это EDA по сути (см. Communication & API Style): communication через события, а не request/response. - цена честная — eventual consistency. Состояние сходится не мгновенно. С этим надо уметь жить: идемпотентность, дедупликация, обработка дубликатов и переупорядочивания.

Починить границы

Распределённую транзакцию на честном слове заменяешь явным паттерном. - saga (Garcia-Molina, Salem, 1987) — длинная транзакция как цепочка локальных, у каждой шаг компенсации на случай отката. Atomicity между сервисами это не даёт — даёт eventual consistency через компенсации. - choreography против orchestration — кто дирижирует сагой: сами сервисы по событиям или отдельный координатор (см. Communication & API Style). - если границы изначально кривые — не подпирай, а перенарезай по bounded context. Иногда дешевле схлопнуть два сервиса обратно в один, чем держать между ними сеть.


Когда это вообще не проблема

Не каждая синхронная связь — distributed monolith. Паника здесь так же вредна, как и беспечность.

  • Если связь честно слабая и сервисы выкатываются независимо — это нормальные сервисы, не анти-паттерн.
  • Иногда обратный ход правильный: схлопнуть распределённый монолит назад в modular monolith (см. микросервисы) — один deploy, но строгие модульные границы внутри. Часто это и есть конечная точка, а не временный костыль.
  • Микросервисы оправданы, когда нужна реальная автономия команд и независимое масштабирование — и когда ты уже знаешь границы. До этого монолит честнее.

Как части общаются: sync vs async

Когда система распадается на отдельные deployable units, появляется вопрос, который раньше решал компилятор: как один кусок зовёт другой. Внутри процесса это вызов функции — миллисекунды, типобезопасно, либо отработало, либо стектрейс. Через сеть всё иначе. И первый выбор тут не протокол, а модель: ждать ответ или не ждать.


Synchronous request/response

Synchronous request/response — это модель, где вызывающий шлёт запрос и блокируется, пока не придёт ответ.

Так работают REST поверх HTTP и gRPC. A зовёт B, держит открытое соединение, ждёт. Пока B думает, логическая операция A занята этим ожиданием.

   ┌─────┐   request    ┌─────┐
   │  A  │ ───────────► │  B  │
   │     │              │     │  B обрабатывает
   │ wait│ ◄─────────── │     │
   └─────┘   response   └─────┘
   A заблокирован всё время, пока B не ответит

Ключевое свойство — temporal coupling, связность во времени.

Что это значит: - A и B должны быть живы одновременно. B лежит — A не получает ответ, операция падает прямо сейчас. - Отказ вызываемого = отказ цепочки. A зовёт B, B зовёт C, C тормозит — тормозит вся цепочка, latency складывается. - Связность не только по доступности, но и по скорости. Медленный B делает медленным A, даже если сам A быстрый.

Зато модель простая: запросил — получил результат — поехал дальше. Никакого промежуточного состояния, ответ либо есть, либо явная ошибка.

Когда подходит: - Нужен немедленный результат, без него дальше нельзя. Логин, проверка баланса, валидация на лету. - Запрос-ответ по своей природе: «дай пользователя по id», «посчитай и верни». - Цепочка короткая, вызываемый надёжный.

Грабли: - Длинные синхронные цепочки A→B→C→D — latency и вероятность отказа перемножаются. - Без таймаутов, retry и circuit breaker один медленный сервис подвешивает всех, кто его зовёт. - Соблазн звать синхронно там, где ответ на самом деле не нужен сразу.


Asynchronous messaging / events

Asynchronous messaging — это модель, где отправитель кладёт сообщение в broker и идёт дальше, не дожидаясь, пока его кто-то обработает.

Между отправителем и получателем встаёт посредник — очередь или broker (Kafka, RabbitMQ, SQS). Отправитель делает fire-and-forget: положил и забыл. Получатель забирает, когда готов.

   ┌─────┐  publish   ┌──────────┐   consume   ┌─────┐
   │  A  │ ─────────► │  broker  │ ──────────► │  B  │
   └─────┘            │ ┌──┬──┬─┐│             └─────┘
   A свободен сразу   │ └──┴──┴─┘│   B читает в своём
                      │  буфер   │   темпе, когда жив
                      └──────────┘

Broker разрывает temporal coupling.

Что это значит: - A и B не обязаны быть живы одновременно. B лежит — сообщения копятся в очереди, B поднимется и разгребёт. - Broker работает буфером. Прилетел пик нагрузки — A не падает, очередь растёт, B доедает в своём темпе. Backpressure вместо каскадного отказа. - Низкая связность: A не знает, кто и сколько получателей читает. Подписался новый consumer — A не трогаешь.

Цена: - eventual consistency. В момент отправки результат ещё не случился. Состояние системы сходится позже, не мгновенно. Под это надо проектировать осознанно, а не надеяться. - Сложнее отладка и трассировка: поток разорван во времени, причинно-следственная связь не видна в одном стектрейсе. - Доставка не бесплатна по гарантиям: at-least-once, дубли, переупорядочивание — обычная реальность брокеров. Получатель почти всегда должен быть идемпотентным.

Когда подходит: - Интеграция между bounded contexts: «заказ оформлен» — пусть реагирует кто хочет. - Развязка и независимость: producer не должен знать и ждать consumer'ов. - Сглаживание пиков: загрузка, рассылки, обработка, которую не жалко отложить на секунды. - Fan-out: одно событие — много независимых реакций.


Где живёт логика: smart endpoints vs ESB

Один и тот же async строят двумя противоположными способами. Разница не в технологии, а в том, где сидит ум системы.

Smart endpoints, dumb pipes — подход микросервисов: вся логика в сервисах, транспорт между ними максимально тупой.

  • Канал умеет одно — доставить сообщение. Маршрутизация, трансформация, бизнес-правила — внутри endpoint'ов.
  • Broker или HTTP — это «труба», в неё не зашивают поведение.
  • Канон микросервисов (Lewis & Fowler, 2014) формулирует это прямым лозунгом.

ESB (enterprise service bus) — подход классической SOA: ум выносят в шину посередине.

  • Шина маршрутизирует, трансформирует форматы, оркестрирует, применяет правила. Сервисы по краям тонкие.
  • Это «smart pipes, dumb endpoints» — зеркальное отражение.
  • На практике шина пухнет, становится центральной точкой отказа и узким местом релизов. Меняешь правило — трогаешь общий компонент, который держит всех.

Чем отличается: - В микросервисах сложность распределена по endpoint'ам, транспорт глупый и дешёвый. - В SOA/ESB сложность централизована в шине, endpoint'ы глупые, шина — тяжёлая.

Канон vs индустрия: - SOA как стиль (Natis & Schulte, Gartner, 1996) не требует ESB. ESB — одна из реализаций, та самая, которую чаще всего и критикуют. - «SOA = SOAP/WSDL/тяжёлая шина» — частое искажение. Стиль про сервисы с контрактами, а не про конкретный стек.


Инфраструктура связи: gateway и discovery

Когда deployable units много, между ними и снаружи нужны вспомогательные кирпичи. Это не модели коммуникации, а обвязка, которая делает их рабочими.

API Gateway — это единая точка входа, которая стоит перед набором сервисов и берёт на себя сквозные задачи.

  • Маршрутизация запроса к нужному сервису, агрегация ответов, трансляция протокола.
  • Cross-cutting в одном месте: auth, rate limiting, TLS-терминация, логирование.
  • Клиент снаружи видит один адрес, а не зоопарк сервисов.
  • Грабля — не путать с load balancer (тупое распределение трафика) и с service mesh (трафик сервис-сервис внутри). И не превращать gateway в «God gateway» с бизнес-логикой.

Service Discovery — это механизм, через который сервис находит сетевой адрес другого, не зная его заранее.

  • В динамике (контейнеры, автоскейлинг) адреса меняются постоянно. Хардкодить IP бессмысленно.
  • Сервис при старте регистрируется в реестре; вызывающий спрашивает реестр «где сейчас B».
  • Снимает связность по адресам: знаешь имя сервиса, а не где он физически поднят.

Как выбирать

Дефолт грубо такой: нужен ответ прямо сейчас, чтобы продолжить, — sync. Нужно сообщить о факте и развязать стороны — async. Большинство реальных систем смешивают обе модели, а не выбирают одну навсегда.

Критерий Sync (request/response) Async (messaging/events)
Связность во времени temporal coupling — оба живы сразу развязка через broker
Результат немедленный eventual consistency
Отказ вызываемого падает вся цепочка сообщения копятся в очереди
Пик нагрузки каскадный отказ буфер, backpressure
Связность сторон A знает B A не знает получателей
Отладка один стектрейс разорванный во времени поток
Типичный кейс запрос-ответ, валидация интеграция, события, fan-out

Sync связывает во времени, async развязывает ценой eventual consistency. Сначала решаешь модель, потом протокол.

Полный разбор стилей и протоколов (REST, GraphQL, gRPC, WebSocket, MQTT и др.) — Communication & API Style.


Эволюция и выбор: Monolith-first, Strangler Fig, закон Конвея

Архитектура на оси «один deployable unit ↔ много» — не точка, а траектория. Систему почти никогда не проектируют на финальной топологии сразу. Её туда двигают по мере роста. Вопрос не «монолит или микросервисы», а «куда и когда сдвигаться».

Двигают по оси два честных драйвера: масштаб (нагрузка, размер кодовой базы) и организация (число команд, как они общаются). Хайп — не драйвер. Если двигаешь систему, потому что «все на микросервисах», — обычно платишь цену распределённости без выгоды.


MonolithFirst: начинай с монолита, дроби потом

MonolithFirst — это эвристика Мартина Фаулера (2015): новое приложение почти всегда лучше начать монолитом и резать на сервисы позже, когда границы прояснились.

Зачем именно так: - На незнакомом домене не знаешь, где пройдут настоящие границы. Любая нарезка — гадание. - Ошибся с границей внутри монолита — двигаешь модуль рефакторингом, дёшево. - Ошибся с границей в микросервисах — двигаешь данные и сетевые контракты между процессами, дорого. - Микросервисы добавляют distributed-цену сразу: сеть, частичные отказы, eventual consistency. За незнание границ платишь этой ценой авансом.

Грабли, которые ловит правило: - Greenfield на микросервисах — частый провал. Силы уходят на инфраструктуру распределёнки, пока сам домен ещё плывёт. - Граница, нарезанная рано, часто оказывается неправильной — и переделывать её через сеть больно.

Канон vs индустрия: - Это эвристика с явными исключениями, не закон. Фаулер сам оговаривает: домен хорошо знаком (вторая система того же класса) или команд заведомо много — можно стартовать иначе. - Частое искажение: MonolithFirst читают как «микросервисы — это плохо / всегда преждевременно». Это не так. Правило про дефолт и про порядок шагов, не про запрет.

Начать монолитом — не значит начать комом грязи. Чистый модульный монолит (Modular Monolith) — хороший старт: границы прочерчены модулями, но всё в одном процессе. Когда модуль реально просится наружу, его легко вынести. Детали нарезки — в Микросервисы.


Strangler Fig: миграция обвитием, без big-bang

Strangler Fig Application — это стратегия миграции Фаулера (2004): новую систему наращиваешь по краям старой, постепенно перенаправляя функциональность, пока легаси не отомрёт целиком.

Идея простая: - Перед монолитом ставишь фасад (роутер/прокси), через который идёт весь трафик. - Новые куски пишешь снаружи как отдельные сервисы. Фасад роутит на них. - Старый код в монолите перестаёт получать трафик по этим путям — отмирает. - Повторяешь, пока от монолита ничего не остаётся. Тогда выключаешь.

Название — от фикуса-душителя: он обвивает дерево-хозяина, постепенно перехватывает его место, старое внутри отмирает. Дерево не рубят одним ударом.

Шаг 0: всё в монолите
  client ──> [ MONOLITH: A B C D ]

Шаг 1: фасад перед монолитом, трафик идёт как был
  client ──> [ facade ] ──> [ MONOLITH: A B C D ]

Шаг 2: вынесли A наружу, facade роутит /A на новый сервис
  client ──> [ facade ] ─/A──> [ svc A ]
                        \─────> [ MONOLITH: ~~A~~ B C D ]   (A внутри отмер)

Шаг N: вынесли всё, монолит пуст — гасим
  client ──> [ facade ] ─/A──> [ svc A ]
                        ─/B──> [ svc B ]
                        ─/C──> [ svc C ]
                        ─/D──> [ svc D ]

Плюсы: - Нет big-bang переписывания. Старое и новое живут параллельно, риск размазан по шагам. - На каждом шаге система рабочая. Откатить кусок дёшево — вернул роут на монолит. - Бизнес не замораживается на «полгода переписываем, фич не будет».

Грабли: - Фасад становится критичной точкой. Он же — место, где копится сложность роутинга. - Двойная поддержка на время миграции: и старый путь, и новый живут одновременно. - Самая частая ошибка — путать с rewrite/big-bang cutover. Суть в постепенности и в том, что обе системы крутятся параллельно. Не «переписали и переключили рубильник». - Имя дрейфовало: исходно Strangler Application, позже Strangler Fig. Встретишь оба.


Закон Конвея: архитектуру диктует оргструктура

Закон Конвея — это наблюдение Мелвина Конвея (1968): организация проектирует систему, чья структура повторяет структуру коммуникации внутри самой организации.

Что это значит на практике: - Четыре команды пишут компилятор — получится четырёхпроходный компилятор. Не потому что так лучше, а потому что так общаются люди. - Граница сервиса тяготеет лечь там, где проходит граница между командами. Где люди общаются редко, рождается слабое coupling и явный контракт. - Это главный нетехнический драйвер движения по оси. Сколько команд и как они общаются — предсказывает топологию сервисов лучше, чем технические аргументы.

Inverse Conway Maneuver: - Идея: раз структура системы повторяет структуру команд — сначала перекрои команды под желаемую архитектуру, и архитектура подтянется сама. - Хочешь N независимых сервисов — заведи N автономных команд с чёткими границами ответственности. - Оговорка: сам Конвей этого манёвра не предлагал. Он описал наблюдение, не тактику. Inverse Conway — позднейшая надстройка индустрии поверх его закона.

Грабли и искажения: - Закон читают как строгий детерминизм. Это эмпирическое социологическое наблюдение, не теорема. Тенденция, не гарантия. - Игнорировать его дорого: рисуешь красивую сервисную нарезку поперёк реальных команд — и она расползётся обратно по линиям общения людей. Топология сопротивляется оргструктуре.


Bounded Context: чем резать

Закон Конвея говорит, что нарезка пойдёт по командам. Но по какому смысловому шву резать сам домен? Критерий даёт DDD.

Bounded Context — это явная граница, внутри которой одна модель домена и её ubiquitous language определены и согласованы (Эрик Эванс, 2003).

Зачем это критерий нарезки: - За границей контекста те же слова значат разное. «Заказ» в продажах и «заказ» на складе — разные модели. Это естественный шов, по которому домен распадается без боли. - Резать сервисы по bounded context — значит резать там, где модель и так разорвана смыслом, а не там, где удобно технически.

Грабли: - Bounded context отождествляют один-к-одному с микросервисом. Это языковая/модельная граница, не deployment unit. Один контекст может жить как несколько сервисов или внутри монолита модулем. - Его же путают с командой и с subdomain. Это связанные, но разные вещи.

Хорошая нарезка — там, где bounded context (смысловой шов) совпадает с границей команды (закон Конвея). Совпало — граница устойчивая. Разошлось — жди трения. Как контекст превращается в сервис и где это окупается — в Микросервисы.


Куда это всё сходится

По оси «один deployable unit ↔ много» систему двигают масштаб и организация, не мода.

  • Не знаешь домен — начинай монолитом (лучше модульным). MonolithFirst.
  • Мигрируешь легаси — обвивай, не переписывай разом. Strangler Fig.
  • Решаешь, где резать — смотри на bounded context (смысл) и на команды (закон Конвея) сразу.
  • Двигаешься, потому что «все так делают» — почти наверняка платишь distributed-цену зря.

Сравнительная таблица

Один взгляд на все стили сразу. Колонки выбраны под главный вопрос макро-архитектуры: на сколько кусков режется деплой и кто чем платит. Разбор каждого стиля — в микросервисы, разбор коммуникации — в Communication & API Style.

Стиль Deployable units Данные Коммуникация Связность (coupling) Когда брать Главная цена
Monolith один артефакт одна общая БД вызов функции в процессе высокая внутри по умолчанию старт, маленькая команда, неясные границы масштабируется только целиком
Modular Monolith один артефакт одна БД, модули по схемам внутрипроцессно, через границы модулей низкая между модулями нужна чистота границ без сети дисциплину границ держишь сам
SOA сервисы + общий middleware у сервисов, часто общая через ESB/брокер, контракты средняя, узел — шина enterprise, интеграция разнородного ESB как single point и bottleneck
Microservices много мелких, независимо каждый владеет своей сеть: HTTP/gRPC + async низкая, decentralized data зрелые команды, разный scale-out распределённость во всём: latency, отладка, данные
SCS несколько крупных вертикалей у каждой своя через UI/links, async; синхронного избегают очень низкая между системами продукт из автономных кусков крупная гранулярность, дублирование UI
Serverless/FaaS функция на событие внешняя (managed/BaaS) события, триггеры, очереди низкая по коду, высокая к провайдеру спайки, событийная нагрузка, no-ops cold start, vendor lock-in, ephemeral state
EDA продюсеры и консьюмеры у каждого своя, синхронизация через события async через broker, pub/sub низкая по времени, через контракт события слабая связь, реакция на изменения eventual consistency, отладка потоков событий

Колонки не ортогональны. Modular Monolith и Microservices различаются по Deployable units, но почти совпадают по Связность — разница в том, проходит граница по сети или по вызову функции. EDA — не альтернатива Microservices, а способ их связать: часто оба слова про одну систему.


Как читать таблицу

Главная ось — Deployable units. Один артефакт против многих определяет почти всё остальное: как масштабируешь, как деплоишь, где ломается консистентность.

один артефакт ───────────────────────────────► много артефактов
Monolith   Modular Monolith        SCS      SOA     Microservices
                                                      + EDA как клей

Вторая ось — где живут данные. По ней Microservices и SCS уходят дальше всех: каждый кусок владеет своим хранилищем, общей БД нет. Это и есть настоящая граница, а не число процессов. Система из двадцати сервисов на одной общей БД — это distributed monolith, не Microservices (разбор в микросервисы).

Связность важнее числа сервисов. Можно нарезать на сто кусков и получить связность монолита плюс цену сети — худшее из двух миров.


Когда что брать (без фанатизма)

Дефолт — Modular Monolith. Один deployable, внутри строгие модули с явными границами. Дробишь на distributed-топологию только тогда, когда появилась причина, которую можно назвать словами. Не «так модно», не «у Netflix так». Подробно про модули внутри — в микросервисы, про связь между сервисами — в Communication & API Style.

Почему дефолт именно Modular Monolith

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

  • один deployable — один деплой, одна транзакция БД, отладка в одном процессе.
  • границы есть, но они in-process — переставить модуль дёшево, пока он не за сетью.
  • это не «временная стадия». Modular Monolith — валидный конечный пункт, а не черновик микросервисов.

Канон vs индустрия: Simon Brown оформил термин (talk «Modular Monoliths», Devoxx 2016); та же идея есть как «The Missing Chapter» в Clean Architecture Роберта Мартина (2017). Дефолт-эвристику «начинай с монолита» Fowler формулирует в MonolithFirst (2015) — это совет, а не закон.

        Modular Monolith                      раздробил рано
  ┌───────────────────────────┐         ┌─────┐  ┌─────┐  ┌─────┐
  │  [orders] [billing] [auth] │   -->   │ ord │--│ bil │--│auth │
  │   ↕ in-process вызовы      │         └─────┘  └─────┘  └─────┘
  └───────────────────────────┘          сеть между тем, что
   границу подвинуть дёшево               ещё меняется вместе

Симптом → что брать

Бери конкретную топологию, когда видишь конкретный симптом. Не наоборот.

  • одна команда, домен ещё плывёт, granularity неясна → Modular Monolith.
  • команда выросла, мешаете друг другу в одном репо и деплое → дроби на services по bounded context, не по слоям.
  • разным частям нужен независимый scale (одна CPU-bound, другая throughput-bound) → выдели именно эти части в отдельные services.
  • разным частям нужен разный runtime (Python для ML, Go для горячего пути) → отдельный deployable там, где это реально окупается.
  • куски системы развязаны по времени, есть пики, producer не должен ждать consumer → EDA, события через broker.
  • крупные автономные вертикали со своим UI, своими данными, минимум синхронной связи → Self-Contained Systems.
  • несколько разных клиентов (iOS, Android, web), каждый тянет свою форму данных → BFF, по одному на UX, тонкий слой агрегации без домена.
  • спорадическая event-нагрузка, glue между сервисами, cron-задачи, обработка загрузок → serverless / FaaS.

Где тебя ждёт overengineering

Дорогие ошибки почти всегда — это распределение там, где хватило бы вызова в памяти.

  • Преждевременные микросервисы. Раздробил до того, как понял границы — получил distributed monolith. Сервисы есть, а деплоятся и меняются вместе. Худшее из двух миров: цена сети плюс coupling монолита. Падает не из-за числа сервисов, а из-за связности контрактов и данных.
  • Дробление по техническим слоям. Сервис «контроллеры», сервис «бизнес-логика», сервис «БД» — это three-tier, разнесённый по сети. Каждый бизнес-запрос дёргает все три по сети. Режь по bounded context (вертикали), не по слоям.
  • EDA там, где хватило бы вызова. Событие, на которое всегда подписан ровно один consumer, и ответ нужен синхронно — это request/response, переодетый в broker. Получил eventual consistency и отладку по логам брокера на ровном месте.
  • Serverless под постоянный low-latency трафик. FaaS эфемерен: cold start, лимит времени, stateless. Под ровный нагруженный hot path это обычно дороже и медленнее обычного сервиса. Serverless любит спорадику, не постоянку.
  • SCS/микросервисы на маленькую команду. Несколько вертикалей с отдельными UI и данными требуют людей на эксплуатацию. На команду из пяти человек это налог без отдачи.

Цена дробления (плати осознанно): - сеть вместо вызова — latency, ретраи, частичные отказы. - распределённые данные — нет одной транзакции, привет saga и eventual consistency. - эксплуатация — трейсинг, версионирование контрактов, отдельные пайплайны на каждый сервис.

Note

Conway's Law (Melvin Conway, 1968) работает фоном: топология сервисов почти всегда повторяет структуру команд. Дробишь систему, не дробя организацию, — границы расползутся обратно по линиям реального общения людей.


Если коротко:

  • Greenfield, домен плывёт → Modular Monolith, границы in-process.
  • Команда выросла, мешаете друг другу → дроби по bounded context, не по слоям.
  • Нужен независимый деплой/scale разных частей → microservices ровно для этих частей.
  • Разный runtime под разные части (ML vs hot path) → отдельный deployable там, где окупается.
  • Развязка по времени, пики, producer не ждёт consumer → EDA через broker.
  • Крупные автономные вертикали со своим UI и данными → Self-Contained Systems.
  • Несколько разных клиентов с разной формой данных → BFF, по одному на UX.
  • Спорадическая event-нагрузка, glue, cron → serverless / FaaS.
  • Сервисы деплоятся и меняются всегда вместе → ты собрал distributed monolith, схлопывай обратно.

Источники

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


Организация и домен

  • Melvin Conway — «How Do Committees Invent?» (Datamation, 1968): откуда растёт Conway's Law. Эмпирическое наблюдение, не предписание: структура системы повторяет структуру коммуникаций команд. Имя «закон» навесил позже Fred Brooks в «The Mythical Man-Month» (1975). Inverse Conway Maneuver — поздняя надстройка, сам Conway его не предлагал.
  • Eric Evans — «Domain-Driven Design» (2003): bounded context как граница модели и ubiquitous language. Не маппится один-к-одному ни на microservice, ни на команду, ни на subdomain — это граница смысла, не deployment unit.

Стили деплоя

  • Martin Fowler — «MonolithFirst» (2015, martinfowler.com): начинай с monolith, режь на сервисы, когда границы понятны. Дефолт с исключениями, не закон. Здесь же канонично оформлена рамка monolith vs microservices — слово monolith это retronym из эпохи микросервисов, а не «грязный код».
  • James Lewis & Martin Fowler — «Microservices: a definition of this new architectural term» (2014, martinfowler.com): каноническое определение стиля. Акцент на independent deployability, decentralized data и organization around business capabilities — не на размере. Термин устаканили на воркшопе архитекторов под Венецией: май 2011, добили май 2012.
  • Mary Shaw & David Garlan — «Software Architecture: Perspectives on an Emerging Discipline» (1996): ранний каталог стилей, где разведены logical layers и physical tiers. Layered живёт внутри одного process, three-tier — это отдельные deployable tiers. Не синонимы.
  • Yefim Natis & Roy Schulte (Gartner) — «Service Oriented Architecture» (1996): первоисточник термина SOA. Это философия дизайна, не стек: SOAP/WSDL/WS-* и тяжёлый ESB — одна (часто критикуемая) реализация, не определение.
  • Simon Brown — доклад «Modular Monoliths» (Devoxx 2016) + simonbrown.je: modular monolith как один deployable с принудительными границами модулей внутри. Не «monolith с папками» и не обязательно ступенька к микросервисам — может быть валидным конечным состоянием. Та же идея — «The Missing Chapter» в «Clean Architecture» Роберта Мартина (2017).
  • Stefan Tilkov / INNOQ — Self-Contained Systems, scs-architecture.org (~2015): SCS как автономный вертикальный срез с собственным UI, логикой и данными. Намеренно крупнее микросервисов (мало на систему), интеграция через UI/links и async, а не синхронный RPC.
  • Mike Roberts & John Chapin — «Serverless Architectures» (2016/2018, martinfowler.com): разбор serverless/FaaS как стиля. Серверы никуда не делись — их прячет и автоскейлит провайдер. Отправная точка по индустрии — запуск AWS Lambda, ноябрь 2014.
  • Carl Hewitt, Peter Bishop, Richard Steiger — «A Universal Modular ACTOR Formalism for Artificial Intelligence» (IJCAI, 1973): первоисточник actor model. Никаких гарантий порядка, доставки или распределённых транзакций из коробки — это потом дописали фреймворки, и location transparency сетевые сбои не отменяет.
  • Frank Buschmann et al. — «Pattern-Oriented Software Architecture, Vol. 1» (1996), паттерн «Pipes and Filters»: поток данных через цепочку независимых stateless-фильтров. Концептуальные корни — Unix pipes (Douglas McIlroy, Bell Labs, реализация 1973). Современные распределённые стримы переиспользуют идею, но добавляют parallelism, backpressure, fault tolerance.

Миграция и анти-паттерны

  • Martin Fowler — «StranglerApplication», позже «Strangler Fig Application» (2004, martinfowler.com): постепенная замена legacy по краям, обе системы крутятся параллельно. Не big-bang переписывание. Имя дрейфовало от Strangler Application к Strangler Fig.
  • Ben Christensen — доклад «Don't Build a Distributed Monolith» (Microservices Practitioner Summit, январь 2016): популяризация термина distributed monolith. Раньше уже встречался у Derek Gottlieb («Distributed Monoliths», блог, июнь 2015), так что Christensen популяризировал, а не придумал. Беда не в числе сервисов, а в coupling: дробление без independent deployability и decoupled data даёт худшее с обеих сторон.
  • Sam Newman — «Building Microservices» и «Monolith to Microservices» (O'Reilly, 2019): практика декомпозиции, BFF, и дальнейшая раскрутка термина distributed monolith.

Интеграция, gateway, frontend

  • Gregor Hohpe & Bobby Woolf — «Enterprise Integration Patterns» (2003): каталог messaging-паттернов (channels, routers, translators, endpoints). Vendor-neutral словарь, не привязан к конкретному ESB и не только про legacy SOA — ложится и на современные event/stream системы.
  • Phil Calçado — «The Back-end for Front-end Pattern (BFF)» (2015, philcalcado.com): практика SoundCloud (идею Calçado приписывает коллеге Lukasz Plotnicki). Один BFF на каждый distinct UX (iOS, Android, web), тонкий слой агрегации и трансляции — не общий god-gateway и не место для domain logic.
  • Chris Richardson — microservices.io и «Microservices Patterns»: каталог паттернов, включая API Gateway (~2014, корни в reverse-proxy и SOA gateway). Не путать gateway с load balancer или service mesh, и не раздувать его в «God gateway» с бизнес-логикой.

Данные, события, согласованность

  • Roy Schulte и аналитики Gartner — Event-Driven Architecture (~2003): компоненты общаются, порождая и реагируя на события, а не через прямой request/response. Единого seminal paper нет, идея кодифицирована в т.ч. в Hohpe & Woolf. EDA это не «просто message queue»; thin event notification, event-carried state transfer и event sourcing часто путают.
  • Greg Young — CQRS (~2010): разделение моделей для команд и запросов. Опирается на Command-Query Separation Бертрана Мейера («Object-Oriented Software Construction», 1988). Разделение на уровне модели, может быть in-process — не обязательно две базы плюс event sourcing. Ключевая популяризация — bliki-статья Фаулера «CQRS» (2011).
  • Martin Fowler & Greg Young — Event Sourcing (Fowler bliki, 2005; практика Young ~2006–2010): источник истины это лог событий, текущее состояние выводится реплеем. С CQRS независимы, хоть и часто ходят вместе. Лог это не мутабельная audit-таблица — versioning событий и цена реплея всплывают позже.
  • Hector Garcia-Molina & Kenneth Salem — «Sagas» (SIGMOD '87): первоисточник. Long-lived transaction как цепочка локальных с компенсациями. ACID между сервисами не даёт — только eventual consistency через компенсацию, не isolation. Оригинал 1987 года был про long-lived DB-транзакции; распределённая orchestration/choreography — поздняя переинтерпретация.

Если этот раздел расходится с тем, что в основном тексте (микросервисы, Communication & API Style) — верь тексту и правь этот список, а не наоборот.


Главное правило: на оси «один deployable ↔ много» тебя двигают масштаб и оргструктура (закон Конвея), не мода. Дефолт — modular monolith. Каждый сетевой разрез покупаешь ценой latency, частичных отказов и eventual consistency — режь по bounded context и ровно там, где боль уже реальная.