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 и ровно там, где боль уже реальная.