Архитектурные паттерны: карта трека и как выбирать архитектуру
Эта статья — вход в трек. Она не пересказывает остальные двенадцать материалов, а даёт то, без чего они превращаются в набор модных слов: систему координат. После неё вы сможете ответить про любую систему на три вопроса: какие решения здесь архитектурные, какими силами они продиктованы и когда их можно будет пересмотреть.
1. Что такое архитектура и почему это не «схема с квадратиками»
Самое рабочее определение принадлежит Ральфу Джонсону, его любит цитировать Мартин Фаулер в эссе «Who Needs an Architect?»:
Архитектура — это те решения, которые разработчики считают трудноизменяемыми, и общее понимание системы, разделяемое опытными разработчиками.
Здесь два независимых утверждения, и оба важны.
Первое: архитектура = множество дорогих в отмене решений. Переименовать метод — не архитектура: цена отмены близка к нулю. Сменить SQL на key-value хранилище, разрезать приложение на процессы, выбрать межсервисный протокол — архитектура: отмена стоит месяцы. Amazon называет это «дверями в одну и в две стороны»: обратимые решения принимаются быстро и локально, необратимые — медленно, с записью и явным разбором альтернатив.
Второе: архитектура — это разделяемая модель. Если пять инженеров рисуют пять разных схем одной системы, архитектуры нет, даже если есть диаграмма в Confluence. Отсюда ценность общего языка: C4 model для уровней детализации, arc42 для структуры описания, ADR для истории решений.
Есть и третье, эмпирическое определение, удобное для оценки: архитектура определяет форму кривой стоимости изменений. Хорошая держит цену типового изменения примерно постоянной по мере роста системы; плохая — делает её растущей.
Обратите внимание на жёлтую кривую: переинженерия — это не «безопасно, но дороже», это тоже проигрыш. Вы заплатили за гибкость по осям, по которым система так и не менялась, и одновременно замедлили обучение — потому что каждая гипотеза проверяется дольше.
2. Карта трека
Маршруты чтения под задачу: навести порядок в коде — 01 → 02 → 11; решить, резать ли монолит — 02 → 03 → 06 → 11; спроектировать интеграции — 07 → 04 → 06; выдержать нагрузку — 08 → 09 → 12; данные и консистентность — 05 → 06 → 04; готовиться к system design интервью — 12 → 03 → 08 → 09. Полный список:
- https://courses.digitable.life/post/architecture-patterns/01-layered-hexagonal-clean/ — как устроены границы внутри одного приложения.
- https://courses.digitable.life/post/architecture-patterns/02-monolith-and-modular-monolith/ — почему модульный монолит чаще всего правильный ответ.
- https://courses.digitable.life/post/architecture-patterns/03-microservices/ — границы, коммуникация, данные и настоящая цена распределённости.
- https://courses.digitable.life/post/architecture-patterns/04-event-driven/ — брокеры, топики, гарантии доставки, порядок сообщений.
- https://courses.digitable.life/post/architecture-patterns/05-cqrs-and-event-sourcing/ — разделение моделей чтения и записи, журнал событий как источник истины.
- https://courses.digitable.life/post/architecture-patterns/06-saga-and-distributed-transactions/ — что делать, когда двухфазный коммит недоступен.
- https://courses.digitable.life/post/architecture-patterns/07-api-styles/ — REST, GraphQL, gRPC, вебхуки, эволюция контрактов.
- https://courses.digitable.life/post/architecture-patterns/08-caching-and-scaling/ — уровни кэша, инвалидация, шардирование, репликация.
- https://courses.digitable.life/post/architecture-patterns/09-resilience-patterns/ — circuit breaker, retry, bulkhead, backpressure, graceful degradation.
- https://courses.digitable.life/post/architecture-patterns/10-serverless-and-edge/ — модель без серверов и вычисления на краю сети.
- https://courses.digitable.life/post/architecture-patterns/11-architecture-decisions/ — ADR, анализ компромиссов, ATAM, fitness functions.
- https://courses.digitable.life/post/architecture-patterns/12-system-design/ — сквозной практикум: от требований до цифр и схемы.
Трек опирается на соседние курсы: https://courses.digitable.life/post/principles/00-overview/ (связность и зацепление — язык, на котором формулируются причины решений), https://courses.digitable.life/post/design-patterns/00-overview/ (паттерны уровня классов), https://courses.digitable.life/post/ddd/00-overview/ (моделирование домена и границы контекстов) и https://courses.digitable.life/post/data-engineering/00-overview/ (аналитические нагрузки).
3. Оси, а не список стилей
Архитектуру часто воспринимают как выбор из меню: «монолит, микросервисы или serverless». Это ложная дихотомия — на деле вы независимо выбираете положение по нескольким осям:
- Разделение кода — насколько явны границы модулей и запрещены ли обращения внутрь чужого.
- Разделение развёртывания — сколько независимо релизимых юнитов.
- Разделение данных — одна схема на всех или своё хранилище у каждого владельца.
- Синхронность связи — вызов с ожиданием ответа или сообщение.
- Управление процессом — оркестрация (есть дирижёр) или хореография (каждый реагирует сам).
Две первые оси дают самую наглядную картинку:
Ключевой вывод — красный путь. Распределённый монолит получается, когда разделение развёртывания выросло, а разделение кода и данных — нет: сервисы ходят в общую БД, релизятся только вместе, но каждый вызов теперь идёт по сети и может отказать. Все издержки распределённой системы и ни одного преимущества. Отсюда MonolithFirst Фаулера: границы почти никогда не удаётся угадать до встречи с реальными пользователями. Контраргумент Stefan Tilkov тоже верен: если границы известны из домена заранее, монолит-первый создаёт связность, которую резать дороже. Обе позиции сходятся в одном: дорого не количество сервисов, а неверно проведённые границы.
4. Движущие силы: атрибуты качества
Функциональные требования почти не влияют на выбор архитектуры — «показать список заказов» реализуется в любом стиле. Влияют атрибуты качества: характеристики как, а не что. Каталог — ISO/IEC 25010, метод работы с ними — Bass, Clements, Kazman, «Software Architecture in Practice».
Главная ошибка — формулировать их прилагательными. «Система должна быть быстрой и надёжной» — не требование, а пожелание. Требование — сценарий атрибута качества из шести частей:
Источник: 10 000 одновременных пользователей
Стимул: отправляют запрос на оформление заказа
Артефакт: сервис checkout
Условия: обычная нагрузка, один AZ недоступен
Отклик: заказ принят и подтверждён
Мера отклика: p99 <= 400 мс, доля ошибок < 0.1% за 5-минутное окно
Такой сценарий проверяется автоматически и однозначно указывает на решения: «один AZ недоступен» требует репликации и запаса ёмкости, «p99 400 мс» запрещает синхронную цепочку из восьми сервисов, «доля ошибок» задаёт бюджет ошибок в смысле Google SRE. Атрибуты конфликтуют между собой — и работа с этими конфликтами и есть работа архитектора:
| Усиливаем | Обычно платим |
|---|---|
| Доступность через репликацию | Консистентностью (статьи 05, 06) |
| Производительность через кэш | Свежестью данных и сложностью инвалидации (08) |
| Автономность команд через сервисы | Сквозной наблюдаемостью и отладкой (03, 09) |
| Безопасность через изоляцию | Задержкой и стоимостью эксплуатации |
| Скорость первой поставки | Стоимостью изменений через год (график выше) |
Правило, которое стоит запомнить дословно: архитектура — это не выбор лучшего решения, а осознанный выбор того, чем вы платите. Если вы не можете назвать, что стало хуже, — вы просто ещё не нашли, что именно.
5. Закон Конвея как проектный инструмент
Мелвин Конвей (1967) заметил: организации проектируют системы, копирующие структуру их коммуникаций. Это не афоризм, а ограничение, которое ломает архитектуры чаще, чем технологии. Практический вывод — «обратный манёвр Конвея»: если нужна архитектура из N автономных частей, сначала сделайте N команд, владеющих этими частями целиком, вместе с эксплуатацией. Три команды — фронтенд, бэкенд, DBA — физически не смогут поддерживать двадцать автономных сервисов: каждое изменение потребует согласования трёх команд, то есть станет распределённым монолитом на уровне процесса (Team Topologies). Отсюда эвристика для оценки чужого решения: посчитайте, сколько команд должно договориться, чтобы выкатить типовую фичу. Если больше двух — проблема в границах, а не в CI.
6. Как выбирать: явная процедура
Не «интуиция опытного архитектора», а воспроизводимые шаги.
с измеримыми порогами"] --> B{"Известны ли
границы домена?"} B -- "нет, домен новый" --> C["Модульный монолит:
одна БД, строгие модули"] B -- "да, контексты подтверждены" --> D{"Разные требования
к масштабу или релизам?"} D -- "нет" --> C D -- "да" --> E{"Есть платформа:
CI/CD, трассировки, on-call?"} E -- "нет" --> F["Сначала платформа,
иначе распределённый монолит"] --> L E -- "да" --> G["Выделить сервисы
по контекстам"] C --> H{"Появилось узкое место?"} H -- "нагрузка на чтение" --> I["Кэш и реплики, статья 08"] --> L H -- "долгие операции" --> J["Очередь, статья 04"] --> L H -- "конфликт релизов" --> G H -- "нет" --> K["Ничего не менять"] G --> L["Записать ADR
с критерием отката"]
Обратите внимание: почти все ветки ведут не к «микросервисам», а к точечному ответу на конкретное узкое место. Это и есть эволюционная архитектура — решение принимается в момент максимума информации, а не минимума. Сравнение стилей по двум практическим осям — операционной сложности и требуемой автономности команд:
Правый нижний квадрант — то самое состояние, в которое команды попадают чаще всего: операционная сложность распределённой системы уже оплачена, автономность ещё не получена.
7. Считаем, а не спорим: количественный выбор
Спор «монолит против микросервисов» становится продуктивным, когда превращается в таблицу взвешенных оценок. Ниже — маленькая рабочая модель. Она не «решает за вас»: её ценность в том, что она заставляет явно назвать веса и оценки, а потом их обсуждать.
"""Взвешенный выбор архитектурного стиля по атрибутам качества."""
from dataclasses import dataclass
ATTRS = ("скорость_поставки", "простота_эксплуатации", "масштабируемость",
"изоляция_отказов", "автономность_команд", "стоимость_владения")
@dataclass(frozen=True)
class Style:
name: str
scores: dict[str, int] # оценки 0..5 по каждому атрибуту из ATTRS
teams_required: int # сколько автономных команд нужно, чтобы стиль окупился
@dataclass(frozen=True)
class Context:
weights: dict[str, float] # важность атрибутов, сумма произвольна
teams: int # команд с владением end-to-end
platform_maturity: float = 1.0 # 0..1: CI/CD, трассировки, дежурства, IaC
def style(name: str, row: tuple[int, ...], teams: int) -> Style:
return Style(name, dict(zip(ATTRS, row)), teams)
# Оценки — не истина, а стартовая точка для калибровки на своём контексте.
STYLES = [
style("Монолит", (5, 5, 2, 1, 1, 5), 1),
style("Модульный монолит", (4, 4, 3, 2, 3, 4), 2),
style("Сервисы по контекстам", (3, 2, 4, 4, 4, 2), 4),
style("Микросервисы", (2, 1, 5, 5, 5, 1), 8),
]
def evaluate(s: Style, ctx: Context) -> float:
"""Взвешенная оценка стиля с поправкой на организационные ограничения."""
total = sum(ctx.weights.values()) or 1.0
base = sum(s.scores.get(a, 0) * w for a, w in ctx.weights.items()) / total
if ctx.teams < s.teams_required: # организационный разрыв: команд не хватает
base /= s.teams_required / max(ctx.teams, 1)
if s.teams_required > 1: # незрелая платформа бьёт по распределённым
base *= 0.4 + 0.6 * ctx.platform_maturity
return round(base, 3)
def rank(ctx: Context) -> list[tuple[str, float]]:
return sorted(((s.name, evaluate(s, ctx)) for s in STYLES),
key=lambda pair: pair[1], reverse=True)
if __name__ == "__main__":
cases = { # веса перечислены в том же порядке, что и ATTRS
"Стартап, 4 инженера": Context(dict(zip(ATTRS, (5, 4, 1, 1, 1, 4))), 1, 0.3),
"Маркетплейс, 9 команд": Context(dict(zip(ATTRS, (2, 2, 5, 5, 5, 2))), 9, 0.9),
}
for title, ctx in cases.items():
print(title)
for name, score in rank(ctx):
print(f" {score:5.3f} {name}")
Вывод:
Стартап, 4 инженера Маркетплейс, 9 команд
4.312 Монолит 3.715 Микросервисы
1.088 Модульный 3.312 Сервисы по контекстам
0.390 Сервисы 2.865 Модульный монолит
0.150 Микросервисы 2.381 Монолит
Разрыв в первом случае почти тридцатикратный — и это не про «микросервисы плохие», а про то, что при одной команде и незрелой платформе они не могут окупиться в принципе.
Сложность. S — число стилей, A — число атрибутов: evaluate — O(A) по времени и
O(1) по дополнительной памяти, rank — O(S·A + S·log S) по времени и O(S) по памяти.
Числа микроскопические, и это принципиально: модель должна быть достаточно дешёвой, чтобы её
пересчитывали на каждом квартальном ревью с изменившимися весами.
Чего модель не делает: она не знает про регуляторику (требование держать персональные данные в отдельном контуре может перевесить всё), про легаси, про стоимость найма под стек, а оценки взяты «в среднем по индустрии». Её единственная настоящая функция — сделать разногласия видимыми: если два инженера расходятся в выводе, они расходятся в конкретном весе конкретного атрибута, а это уже обсуждаемо.
8. Решения нужно фиксировать: ADR и его жизненный цикл
Архитектура, о которой не осталось письменных следов, деградирует за один цикл ротации команды: новые люди не знают, какие альтернативы уже отвергнуты и почему. Дешёвое противоядие — Architecture Decision Record Майкла Найгарда: одна страница в репозитории на одно значимое решение.
Неизменяемость — не формализм: ценность ADR в истории. «Мы выбрали PostgreSQL, потому что в 2024 у нас не было опыта эксплуатации Cassandra» — это знание о причинах, и именно оно объясняет, почему в 2026 решение можно пересматривать.
Минимальный набор разделов: контекст (цифры, а не ощущения), решение, альтернативы с причиной отказа, последствия — обязательно и плюсы, и минусы — и критерий пересмотра («если лаг консьюмера p99 > 60 с — возвращаемся к вопросу»). Последний раздел отличает живой ADR от бюрократии: он заранее говорит, при каких наблюдаемых условиях решение перестаёт быть верным. Полный формат и ATAM — в https://courses.digitable.life/post/architecture-patterns/11-architecture-decisions/.
9. Архитектура, которую проверяет CI: fitness functions
Решение «домен не зависит от инфраструктуры» живёт ровно до первого спешного релиза, если его не проверяет машина. Идея fitness function из Building Evolutionary Architectures проста: любое архитектурное правило превращается в автотест.
"""Fitness function: домен не должен импортировать инфраструктуру.
Работает на статическом AST: проверяемый код не импортируется и не выполняется —
это важно для CI, где побочные эффекты недопустимы.
"""
import ast
import pathlib
import sys
# Пакет-ключ не имеет права импортировать ни один из пакетов-значений.
FORBIDDEN: dict[str, tuple[str, ...]] = {
"app.domain": ("app.infra", "sqlalchemy", "requests", "boto3", "kafka"),
"app.application": ("app.infra", "sqlalchemy"),
}
def module_name(path: pathlib.Path, root: pathlib.Path) -> str:
"""app/domain/order.py -> app.domain.order"""
rel = path.relative_to(root).with_suffix("")
return ".".join(p for p in rel.parts if p != "__init__")
def imports_of(tree: ast.AST) -> set[str]:
"""Имена всех модулей, импортируемых в файле."""
found: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
found.update(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
found.add(node.module)
return found
def check(root: pathlib.Path) -> list[str]:
"""Список нарушений вида 'модуль -> запрещённый импорт'."""
violations = []
for path in root.rglob("*.py"):
name = module_name(path, root)
banned = tuple(b for pkg, bs in FORBIDDEN.items()
if name == pkg or name.startswith(pkg + ".") for b in bs)
if not banned:
continue
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
for imp in sorted(imports_of(tree)):
if any(imp == b or imp.startswith(b + ".") for b in banned):
violations.append(f"{name} -> {imp} ({path})")
return violations
if __name__ == "__main__": # запуск в CI: python arch_check.py src/
problems = check(pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "."))
print(*(f"ARCH VIOLATION: {p}" for p in problems), sep="\n")
sys.exit(1 if problems else 0)
Сложность. O(N · T) по времени, где N — число файлов, T — средний размер AST (разбор
и один обход на файл), и O(T_max) по памяти — дерево держим по одному файлу за раз. На
репозитории в 5000 файлов это единицы секунд, значит проверку можно ставить в pre-commit.
Такой тест — это исполняемое ADR: когда через год кто-то импортирует SQLAlchemy в доменную модель, сборка упадёт с внятным сообщением. Промышленные аналоги: ArchUnit для Java, NetArchTest для .NET, import-linter для Python, dependency-cruiser для TypeScript. Разумный набор — 5–15 функций: направления зависимостей, запрет доступа к чужой БД, лимит p95 в нагрузочном тесте, размер публичного API модуля, наличие таймаута у каждого HTTP-клиента.
10. Немного истории: почему стили сменяли друг друга
Каждый стиль решал боль предыдущего и создавал новую. Знание цепочки защищает от восприятия свежего термина как прогресса.
Показательна судьба ESB: централизованная шина решала интеграцию, но становилась узким местом и техническим, и организационным — отсюда лозунг микросервисов «smart endpoints, dumb pipes». Через десять лет выяснилось, что сотни мелких сервисов стоят дороже, чем считалось, и маятник качнулся к модульному монолиту (https://courses.digitable.life/post/architecture-patterns/02-monolith-and-modular-monolith/). Ни один из переходов не был «прогрессом вообще» — каждый обменивал одни издержки на другие.
11. Типичные ошибки
- Resume-driven development. Технологию выбрали потому, что её интересно изучить. Признак: в обосновании нет ни одного измеримого сценария атрибута качества.
- Архитектура под нагрузку, которой нет. Шардирование при 200 RPS, Kafka ради трёх событий в минуту. Считайте: 1000 RPS при 20 мс обработки — это ~20 одновременных запросов, один средний сервер. Прикидка «на салфетке» из https://courses.digitable.life/post/architecture-patterns/12-system-design/ экономит годы.
- Границы по техническим слоям вместо доменных. Сервис «валидации», сервис «работы с БД», сервис «нотификаций»: любая фича трогает все три — та же связность, только через сеть. Границы проводят по ограниченным контекстам (https://courses.digitable.life/post/ddd/00-overview/), а слои живут внутри сервиса (https://courses.digitable.life/post/architecture-patterns/01-layered-hexagonal-clean/).
- Общая база у нескольких сервисов. Схема БД становится публичным API без версионирования, любая миграция ломает соседей (https://courses.digitable.life/post/architecture-patterns/03-microservices/).
- Игнорирование сети как источника отказов. «Восемь заблуждений распределённых вычислений» Питера Дойча живы: синхронная цепочка из пяти сервисов с доступностью 99.9% каждый даёт 99.5% — почти четыре часа простоя в месяц (https://courses.digitable.life/post/architecture-patterns/09-resilience-patterns/).
- Отсутствие обратимости. Нет ни флага, ни способа откатиться, ни метрики, по которой станет видно, что решение ошибочно.
- Диаграмма вместо архитектуры. Схема в вики, разошедшаяся с кодом за два спринта; лечится fitness functions и генерацией схем из кода.
- «Мы всё перепишем». Пока вы пишете новую систему, старая набирает фичи. Альтернатива — Strangler Fig: новое растёт рядом, трафик переключается по частям, старое удаляется постепенно.
12. Как это выглядит в проде
- Архитектура — это то, что видно на дашборде. Разговор начинается не со схемы, а с цифр: p50/p99 по эндпоинтам, доля ошибок, лаг консьюмеров, насыщение пулов. Минимальный набор задают «четыре золотых сигнала» из Google SRE: latency, traffic, errors, saturation.
- Обзоры регулярны и дёшевы. Не «архитектурный комитет раз в квартал», а короткий разбор при появлении очередной «двери в одну сторону»: ADR плюс получасовое ревью с соседями.
- Готовность важнее замысла. Фаулер перечисляет предпосылки для сервисов: быстрое provisioning, базовый мониторинг, быстрый деплой — практически это CI/CD, распределённая трассировка, централизованные логи, дежурства, IaC. Без них переход к сервисам гарантированно приводит в правый нижний квадрант из раздела 6.
- Метрики потока важнее метрик кода. DORA выделяет четыре предиктора: частота деплоев, lead time, время восстановления, доля неудачных изменений. Если после архитектурного изменения ни одна не улучшилась — изменение не окупилось.
- Архитектура живёт в бюджете. Managed-сервисы, трафик между зонами доступности, хранение событий — строки в счёте. Стоимость владения — такой же атрибут качества, как задержка.
13. Мини-итог
- Архитектура — множество трудноотменяемых решений плюс разделяемое понимание системы; практический критерий качества — форма кривой стоимости изменений.
- Стиль не выбирается из меню: вы независимо выбираете положение по осям — разделение кода, развёртывания, данных, синхронность связи, оркестрация против хореографии.
- Решения диктуют атрибуты качества, а они конфликтуют: не назвали, чем платите, — значит, ещё не приняли решение. Закон Конвея при этом ограничение первого порядка: архитектура не бывает автономнее структуры команд.
- Фиксируйте решения в ADR с критерием пересмотра, инварианты — в fitness functions в CI.
- Самая дорогая ошибка — распределённый монолит: издержки сети оплачены, автономность не получена.
Источники
- Martin Fowler. Who Needs an Architect? и Software Architecture Guide — определение архитектуры и сводный путеводитель.
- Mark Richards, Neal Ford. Fundamentals of Software Architecture, Software Architecture: The Hard Parts — каталог стилей и техника анализа компромиссов.
- Len Bass, Paul Clements, Rick Kazman. Software Architecture in Practice, 4th ed. — сценарии атрибутов качества, тактики, ATAM.
- Neal Ford, Rebecca Parsons, Patrick Kua. Building Evolutionary Architectures — fitness functions.
- Martin Kleppmann. Designing Data-Intensive Applications — база по данным и распределённым системам.
- Sam Newman. Building Microservices, 2nd ed. и Monolith to Microservices — границы и миграция; Skelton, Pais. Team Topologies — обратный манёвр Конвея.
- Michael Nygard. Documenting Architecture Decisions, каталог шаблонов adr.github.io, нотации C4 и arc42.
- Google SRE Book, AWS Well-Architected, DORA, ISO/IEC 25010.
Что дальше
Координаты есть. Дальше — самый нижний масштаб: как проводятся границы внутри одного приложения, ещё до всяких сервисов. Это фундамент, без которого разделение на процессы бессмысленно: Многоуровневая, гексагональная, луковичная и чистая архитектуры.