Архитектурные шаблоны на практике: как выбрать форму системы
Предыдущие четыре главы разбирали систему по частям: клиент, сервер, данные, интеграция. Эта глава — про то, как эти части складываются в форму целого, и почему форму приходится выбирать осознанно, даже если очень хочется «просто начать писать код».
Практическая проблема выглядит так. Команда садится за новый сервис, и через двадцать минут спор уже идёт не о продукте, а о словах: «давайте сразу микросервисы», «нет, монолит, но с чистой архитектурой», «а зачем нам слой репозиториев», «сделаем на событиях». Каждый участник спора прав по-своему, потому что каждый оптимизирует свой атрибут качества и не проговаривает какой. Задача этой главы — дать общий язык: какие вообще бывают формы систем, чем они отличаются на уровне последствий и по какой процедуре из них выбирают.
Это карта, а не учебник по каждому шаблону. На портале есть трек «Архитектурные шаблоны» из пятнадцати глав, где каждая форма разобрана вглубь — с кодом, схемами развёртывания и разбором отказов. Здесь мы проходим по всем формам сразу, чтобы вы видели пространство вариантов целиком, и в конце каждого разбора говорим, куда идти за деталями.
1. Архитектура — это множество решений, которые дорого менять
Самое рабочее определение принадлежит Ральфу Джонсону, и оно звучит почти цинично: архитектура — это те решения, которые вы хотели бы принять правильно с первого раза, потому что менять их потом дорого. Не диаграммы, не документ и не должность. Критерий один: стоимость изменения решения на поздней стадии.
Отсюда следует важное: «не выбирать архитектуру» невозможно. Если вы не выбрали её сознательно, она сложилась сама — из первых десяти файлов, из привычек фреймворка, из того, кто первым добавил очередь. Разница только в том, знаете ли вы, какие решения приняты, и можете ли назвать их цену.
| Решение | Цена изменения через год |
|---|---|
| Имя переменной, структура функции | Минуты, IDE переименует |
| Внутренняя структура модуля | Часы-дни, покрыто тестами |
| Граница между сервисами | Недели, миграция данных и контрактов |
| Модель согласованности данных | Месяцы, переписывается бизнес-логика |
| Способ развёртывания и релизный цикл | Квартал, меняется процесс всей команды |
Верхние строки — не архитектура, это код. Нижние — архитектура. Практический вывод: тратьте время на анализ ровно там, где стоимость ошибки высокая, и не устраивайте проектных ревью для формы функции.
Архитектурный шаблон — это документированное решение повторяющейся задачи в определённом контексте, вместе с описанием последствий. Три слова тут одинаково важны: задача (что именно болит), контекст (при каких условиях это болит) и последствия (чем вы заплатите за лекарство). Шаблон без контекста и последствий превращается в моду, а мода в архитектуре стоит дороже всего.
2. Шаблон, стиль и паттерн проектирования: три разных слова
Эти три термина путают постоянно, и путаница дорого обходится в разговоре.
Архитектурный стиль — самый широкий уровень: набор ограничений на форму системы. «Клиент-сервер», «многоуровневый», «событийный» — это стили. Стиль не говорит, из каких компонентов состоит ваша система; он говорит, какие связи в ней разрешены.
Архитектурный шаблон — стиль, доведённый до применимого рецепта: перечислены роли компонентов, правила взаимодействия, известные последствия. В литературе граница между стилем и шаблоном размыта, и в 90% разговоров ею можно пренебречь.
Паттерн проектирования (GoF: «Приёмы объектно-ориентированного проектирования») — про форму кода внутри одного компонента: Наблюдатель, Стратегия, Фабрика, Декоратор. Он не влияет на развёртывание, не меняет сетевые границы и не требует согласования с эксплуатацией.
| Архитектурный шаблон | Паттерн проектирования | |
|---|---|---|
| Единица | Компонент, сервис, узел | Класс, функция, модуль |
| Что определяет | Форму системы | Форму кода |
| Кто участвует в решении | Команда, эксплуатация, иногда бизнес | Автор кода и ревьюер |
| Цена ошибки | Недели-месяцы | Часы |
| Виден ли снаружи | Да: в задержках, отказах, релизах | Нет |
Простая проверка: если для смены решения нужно менять схему развёртывания или контракт между командами — это архитектура. Если достаточно рефакторинга в одном репозитории под зелёными тестами — это паттерн проектирования. Разбор самих паттернов — в треке «Паттерны проектирования»; шаблоны и паттерны не конкурируют, а живут на разных этажах: событийная архитектура снаружи прекрасно уживается с Наблюдателем внутри обработчика.
3. Настоящий критерий выбора — атрибуты качества
Функциональные требования почти никогда не диктуют архитектуру: «пользователь оформляет заказ» реализуется и в монолите, и в двадцати сервисах, и в скрипте на Bash. Форму системы определяют нефункциональные требования — атрибуты качества.
Рабочий минимум, который стоит проговорить перед любым выбором:
- Модифицируемость — сколько мест придётся тронуть для типичного изменения. Главный вопрос: какие изменения ожидаются часто?
- Производительность — задержка и пропускная способность на реальном профиле нагрузки.
- Доступность — что происходит при отказе части системы, какой процент запросов выживает.
- Тестируемость — можно ли проверить логику без поднятия половины инфраструктуры.
- Наблюдаемость — можно ли по логам и трассам ответить, где именно сломалось.
- Стоимость владения — счёт за инфраструктуру плюс человеко-часы на поддержку.
- Скорость поставки — сколько времени проходит от коммита до продакшна и сколько команд надо синхронизировать.
Ключевая мысль: шаблон не бывает «хорошим». Он выгоден по одним атрибутам и убыточен по другим, всегда. Многоуровневая архитектура покупает понятность ценой задержки; микросервисы покупают независимость релизов ценой сетевой сложности; событийная архитектура покупает развязку ценой наблюдаемости. Ваша задача — не найти лучший шаблон, а понять, какие атрибуты для вас критичны, и заплатить именно там, где не больно. Как формулировать такие требования измеримо (не «система должна быть быстрой», а «p99 ниже 300 мс при 1000 rps») — в главе «Нефункциональные требования».
Правый нижний квадрант — это не шаблон, а диагноз: операционная сложность распределённой системы уже оплачена, а независимости изменений нет. Про него отдельно в разделе про ошибки.
4. Семь шаблонов в едином формате
Дальше — семь разборов по одной и той же схеме: Контекст → Задача → Решение → Чем платишь → Когда применять → Глубже. Схема взята из книги Басса, Клементса и Кацмана «Software Architecture in Practice» и хороша именно тем, что не даёт спрятать раздел «чем платишь».
4.1. Многоуровневая архитектура (layered)
Контекст. Система растёт, над ней работает больше одного человека, и нужно, чтобы части можно было менять и понимать по отдельности. Обычный набор уровней: представление → прикладная логика → доступ к данным → хранилище.
Задача. Разделить код так, чтобы у каждой группы модулей была понятная роль, а зависимости шли в одну сторону — иначе любая правка тянет за собой всю систему.
Решение. Уровень — это группа модулей, предоставляющих связанный набор услуг через публичный интерфейс. Правило одно и оно жёсткое: использование однонаправленно, верхний уровень знает про нижний, нижний про верхний — никогда. Уровень бывает закрытым (запрос обязан пройти через него) и открытым (можно проскочить мимо). Закрытость — это изоляция: замена хранилища не должна касаться представления. Открытость — это признание того, что для тонкого слоя выгода от прохода не окупает накладных расходов.
Три уточнения, которые чаще всего теряются:
- Уровень — техническое разделение, а не доменное. «Все контроллеры вместе, все репозитории вместе» — это про роль в обработке запроса, а не про предметную область. Отсюда типичная боль: изменение одной бизнес-функции трогает четыре уровня в четырёх каталогах.
- Однонаправленность нужно проверять автоматически, иначе она разрушается за квартал. Линтеры импортов, ArchUnit,
import-linter— дешевле любого код-ревью. - Каждый уровень — это ещё один переход, копирование данных и маппинг DTO. На горячем пути это заметно.
# Нарушение однонаправленности видно по импорту, а не по диаграмме.
# Правило проверяется тестом, а не договорённостью на созвоне.
LAYERS = ["web", "application", "domain", "infrastructure"] # сверху вниз
def check(module: str, imported: str) -> bool:
"""Импорт разрешён только вниз по стеку уровней или внутри своего уровня."""
return LAYERS.index(module) <= LAYERS.index(imported)
assert check("web", "application") # ок: сверху вниз
assert not check("domain", "web") # запрещено: домен знает про веб
Чем платишь. Задержкой на прохождении уровней; размазанностью бизнес-изменения по каталогам; соблазном «анемичного» домена, когда логика утекает в сервисный уровень.
Когда применять. Практически всегда как стартовая точка внутри одного процесса: небольшие и средние приложения, ограниченный бюджет и срок, команда, которой нужен предсказуемый скелет.
Глубже — «Layered, гексагональная и чистая архитектура», а про то, как делить по домену вместо техники — «Ограниченные контексты».
4.2. Ярусы (tiers)
Контекст. Систему нужно физически разложить по машинам: браузер, сервер приложений, СУБД, кеш.
Задача. Разделить систему на независимые в вычислительном отношении исполнительные группы, связанные средствами коммуникации, чтобы масштабировать и защищать их по отдельности.
Решение. Ярус — это группа компонентов, разворачиваемых вместе на одном вычислительном узле. Классическая трёхъярусная схема: клиент, сервер приложений, сервер БД. Ярусы разделяются сетью, значит между ними появляются задержка, сериализация, аутентификация и возможность отказа.
Здесь живёт главная терминологическая путаница портала: layer ≠ tier.
| Layer (уровень) | Tier (ярус) | |
|---|---|---|
| Природа | Логическая: организация кода | Физическая: организация развёртывания |
| Граница пересекается | Вызовом функции | Сетевым запросом |
| Цена пересечения | Наносекунды | Миллисекунды плюс вероятность отказа |
| Меняется | Рефакторингом | Изменением инфраструктуры |
| Можно ли иметь 4 уровня в 1 ярусе | Да, это норма | — |
Четырёхуровневое приложение, развёрнутое одним процессом рядом с базой, — это четыре layer и два tier. Утверждение «у нас трёхуровневая архитектура» почти всегда означает три яруса, и уточнить это стоит в первую минуту разговора.
Чем платишь. Каждая физическая граница добавляет сетевую задержку, режим частичного отказа, отдельный мониторинг и отдельный контур безопасности. Стоимость владения растёт быстрее, чем число ярусов.
Когда применять. Когда для этого есть физическая причина: разный профиль масштабирования (веб-узлов много, база одна), требование изоляции (БД в приватной подсети), географическое разнесение.
Глубже — «System design» про раскладку по узлам, «Прокси и балансировка» про то, что происходит между ярусами, и «Контейнеры и реестры» про современную упаковку яруса.
4.3. Каналы и фильтры (pipes & filters)
Контекст. Есть поток дискретных элементов данных, который нужно последовательно преобразовать от входа к выходу, причём преобразования повторяются из задачи в задачу.
Задача. Разбить обработку на слабо связанные повторно используемые компоненты с единым простым механизмом стыковки — чтобы их можно было переставлять, заменять и выполнять параллельно.
Решение. Фильтры соединяются каналами. Канал однонаправленный, типа «точка — точка»: один источник, один приёмник. Фильтр не знает, кто стоит до и после него, — он знает только формат данных. Классическая классификация фильтров:
- генератор (источник) — начинает поток: чтение файла, курсор по таблице, подписка на топик;
- преобразователь (map) — меняет элементы: нормализация, обогащение, парсинг;
- испытатель (filter/reduce) — отбирает или сворачивает по критерию;
- потребитель (приёмник) — конец потока: запись в хранилище, отправка отчёта.
# Композиция фильтров: каждый шаг — генератор, память O(1) по числу записей,
# время O(n) на проход. Порядок шагов меняется без правки самих фильтров.
from typing import Iterable, Iterator
def parse(rows: Iterable[str]) -> Iterator[dict]:
for row in rows:
name, qty = row.rstrip("\n").split(",")
yield {"name": name, "qty": int(qty)}
def valid(items: Iterable[dict]) -> Iterator[dict]:
for item in items: # испытатель: критерий отбора
if item["qty"] > 0:
yield item
def enrich(items: Iterable[dict], prices: dict[str, int]) -> Iterator[dict]:
for item in items: # преобразователь
yield item | {"total": item["qty"] * prices.get(item["name"], 0)}
pipeline = enrich(valid(parse(open("orders.csv"))), prices={"кофе": 300})
Тот же шаблон вы каждый день видите в шелле: cat access.log | grep 500 | awk '{print $7}' | sort | uniq -c — четыре фильтра и четыре канала. Классический пример из литературы — компилятор: лексический анализ, синтаксический, семантический, генерация кода. Промышленный пример — ETL/ELT-конвейеры и потоковая обработка.
Чем платишь. Плохо подходит для интерактивных сценариев, где нужен диалог, а не проток. Каждый стык требует сериализации и разбора — на длинной цепочке это доминирующая стоимость. Обработка ошибок неочевидна: «куда девать битую запись» приходится проектировать отдельно (на схеме — карантин).
Когда применять. Односторонняя обработка данных: импорт, экспорт, отчётность, конвейеры аналитики, обработка событий без обратной связи.
Глубже — «ETL против ELT» и «Пакетная обработка»; канонический каталог стыков — Enterprise Integration Patterns.
4.4. Клиент-сервер
Контекст. Есть общие ресурсы и услуги, к которым обращается много распределённых потребителей, и нужно контролировать доступ и качество обслуживания.
Задача. Вынести общие услуги в одно место, чтобы менять их централизованно, управлять доступом и масштабировать независимо от числа клиентов.
Решение. Клиенты инициируют запросы и ждут ответ; сервер принимает запросы и отвечает. Асимметрия принципиальная: клиент знает адрес сервера, сервер про клиентов заранее не знает. Отсюда следствия — сервер обязан считать любой вход враждебным, а клиент обязан быть готов к тому, что ответа не будет.
Чем платишь. Сервер — узкое место по производительности и потенциальная единая точка отказа; лечится репликацией и балансировкой, но это уже другая цена. И главное: решение о том, что выполняется на клиенте, а что на сервере, менять после запуска дорого. Перенести валидацию, расчёт цены или рендеринг с одной стороны на другую — это изменение контракта, миграция клиентов, поддержка старых версий приложения на устройствах пользователей неопределённо долго.
Когда применять. Практически любое многопользовательское приложение; вопрос не «применять ли», а «где провести границу и что положить на каждую сторону».
Глубже — главы «Клиентская сторона» и «Серверная сторона» этого трека; про формы контракта между ними — «Стили API».
4.5. MVC и его потомки
Контекст. В интерактивном приложении интерфейс меняется чаще всего остального, а одни и те же данные нужно показывать в нескольких видах одновременно — таблицей, графиком, сводкой — и все виды должны отражать актуальное состояние.
Задача. Отделить логику интерфейса от логики приложения, обеспечив при этом быстрый отклик на действия пользователя и синхронное обновление всех представлений при изменении данных.
Решение. Три роли: Модель хранит данные и правила; Представление отображает часть данных и принимает ввод; Контроллер переводит ввод в операции над моделью. Модель ничего не знает о представлениях и оповещает их об изменениях — исторически через Наблюдателя.
Ключ к пониманию: изначально решалась задача нескольких представлений одних данных (Трюгве Реенскауг, Smalltalk-80, конец 1970-х). Как только представление осталось одно, а обновление стало приходить с сервера, исходная мотивация испарилась — и аббревиатура расползлась:
| Вариант | Что изменилось | Где живёт |
|---|---|---|
| MVC (серверный) | Контроллер = обработчик HTTP-запроса, представление = шаблон | Rails, Django, Spring MVC |
| MVP | Presenter говорит с пассивным View через интерфейс — ради тестируемости | Классический Android, WinForms |
| MVVM | ViewModel плюс двусторонняя привязка данных | WPF, Jetpack Compose, Vue |
| Flux/Redux | Однонаправленный поток, одно хранилище, действия вместо сеттеров | React-экосистема |
| Серверные компоненты | Представление снова считается на сервере, состояние минимально | Next.js RSC, Hotwire, HTMX |
Общий знаменатель у всех пяти один: отделить состояние от отрисовки и назвать место, где происходит изменение. Спор «MVC против MVVM» почти всегда бесполезен; полезен вопрос «где у нас источник истины и кто имеет право его менять».
Чем платишь. Для простого экрана три роли — избыточная церемония. Разделение легко деградирует: контроллер обрастает логикой, которой место в модели, и превращается в файл на две тысячи строк. Абстракции ложатся не на всякий UI-тулкит.
Когда применять. Любой нетривиальный интерфейс. Но выбирайте конкретного потомка по инструменту, а не по красоте аббревиатуры.
Глубже — «Архитектура фронтенда», «Архитектура мобильных приложений» и разбор Наблюдателя в «Поведенческих паттернах». Историю вариантов подробно разбирает Мартин Фаулер в GUI Architectures.
4.6. Событийная архитектура
Контекст. Нужно обрабатывать независимые асинхронные события, поток которых неравномерен, и масштабировать обработку по мере роста нагрузки.
Задача. Построить систему, где отправитель не ждёт получателя и не знает о нём, и где всплеск нагрузки не роняет обработку, а растягивает её во времени.
Решение. Три роли: очередь (буфер и точка развязки), планировщик/диспетчер (достаёт события и решает, кому отдать) и обработчики (независимые процессы, каждый со своей политикой повторов). Отправитель публикует факт о произошедшем и продолжает работу.
Классический сквозной пример — оформление заказа, где два сервиса согласуют результат обменом событиями:
Три вещи, которые видно на этой схеме и которые определяют всю дальнейшую жизнь системы. Во-первых, пользователь получает ответ до того, как результат известен — интерфейс обязан уметь показывать промежуточное состояние. Во-вторых, отмена — это не откат транзакции, а компенсирующее действие: заказ переводится в CANCELLED отдельным шагом, и такая цепочка называется сагой. В-третьих, доставка «хотя бы один раз» — норма, поэтому обработчик обязан быть идемпотентным: повторный CreditReserved не должен резервировать лимит дважды.
Чем платишь. Наблюдаемостью прежде всего: причинно-следственная связь размазана по нескольким журналам, и без сквозного идентификатора трассировки отладка превращается в археологию. Дальше — восстановление после ошибок (что делать с событием, которое не удалось обработать двадцать раз подряд), порядок доставки и дубли, а также сложность тестирования: сценарий не воспроизводится вызовом одной функции.
Когда применять. Отправителю действительно не нужен ответ немедленно; получателей несколько или их число меняется; нагрузка всплесками; шаги долгие или ненадёжные.
Глубже — «Событийная архитектура», «Саги и распределённые транзакции», а про дубли и порядок — «Идемпотентность и гарантии доставки».
4.7. Микросервисы
Контекст. Приложение обслуживает много типов клиентов, над ним работает несколько команд, а монолит стал слишком большим, чтобы его безопасно и часто выкатывать.
Задача. Дать частям системы возможность развиваться и выкатываться независимо друг от друга.
Решение. Система строится как набор сервисов, каждый со своей границей, своим API и — критически важно — своим хранилищем. Сервисы разворачиваются и масштабируются независимо, могут быть написаны на разных языках и принадлежат разным командам.
Единственная настоящая причина выбрать микросервисы — независимое развёртывание. Всё остальное, что им приписывают (модульность, чистые границы, возможность переписать кусок), достигается модульным монолитом дешевле и без сети посередине. Если ваши сервисы всё равно выкатываются одним релизом — вы заплатили полную цену и не купили ничего.
Что ломается при переходе:
- Сеть. Вызов функции превращается в запрос, который может отвалиться, зависнуть или прийти дважды. Нужны таймауты, ретраи, предохранители, бюджеты задержек.
- Данные. Транзакции больше нет: два сервиса не могут закоммитить вместе. Нужны саги, компенсации и жизнь с временной несогласованностью.
- Наблюдаемость. Один пользовательский сценарий — десять записей в разных логах. Без распределённой трассировки диагностика останавливается.
- Когнитивная нагрузка. Инженер держит в голове не «где код», а «в каком сервисе, какой версии, с каким контрактом». Это самая недооценённая статья расходов.
- Эксплуатация. Двадцать сервисов — это двадцать конвейеров, наборов дашбордов, дежурств и планов отката.
Чем платишь. Смотри список выше целиком: он и есть цена. Плюс инфраструктурный счёт и время команды платформы.
Когда применять. Когда независимый релизный цикл частей действительно нужен и оплачен: несколько команд, у которых релизы блокируют друг друга; резко разные профили нагрузки; жёсткая организационная или регуляторная граница.
Глубже — «Микросервисы» и обязательный к прочтению противовес «Монолит и модульный монолит». Первоисточники: Мартин Фаулер, Microservice Trade-Offs и MonolithFirst.
5. Как выбирать: процедура, а не вкус
Выбор формы системы — это не «какой шаблон лучше», а последовательность проверок. Процедура ниже неидеальна, но она заметно лучше спора в чате.
Шаг 0. Начните с монолита по умолчанию. Не потому что монолит хорош, а потому что на старте вы хуже всего знаете границы своего домена, а микросервисы требуют угадать их сразу. Ошибиться с границей внутри монолита — рефакторинг; ошибиться между сервисами — миграция данных.
Шаг 1. Назовите силы, которые реально толкают к разделению. Их всего три, и все три проверяемы:
- Независимый релизный цикл. Две команды регулярно блокируют релизы друг друга — это измеряется в очереди на выкатку, а не в ощущениях.
- Разный профиль нагрузки. Одна часть требует в двадцать раз больше ресурсов или другого железа (GPU, много памяти), и масштабировать всё целиком расточительно.
- Организационная граница. Закон Конвея: система повторяет структуру коммуникаций организации. Если между командами уже есть жёсткая граница — она проявится в коде, хотите вы того или нет. Обратный ход (менять организацию под желаемую архитектуру) — сознательный приём, см. «Топологии команд».
Если ни одна сила не выражена — разделение не окупится.
Шаг 2. Проверьте готовность эксплуатации. Распределённая система требует минимума до первого разделения, а не после: автоматизированный конвейер выкатки, централизованные логи, метрики и распределённая трассировка, дежурство, воспроизводимые окружения. Нет этого — вы получите не микросервисы, а распределённый отказ без диагностики.
Шаг 3. Разделяйте по одному куску. Первым выносится то, у чего яснее всего граница и очевиднее выигрыш: обычно это часть с другим профилем нагрузки или та, что чаще всех меняется.
Отдельно отметим: шаблоны не взаимоисключающие. Реальная система обычно многоярусная снаружи, многоуровневая внутри каждого сервиса, с MVC-подобной раскладкой на клиенте, конвейерами в аналитике и событиями между доменами. Вопрос всегда «какой шаблон где», а не «какой шаблон вместо остальных».
6. Зафиксируйте решение: ADR
Через полгода никто не вспомнит, почему выбрали очередь вместо синхронного вызова, — и решение либо будет пересмотрено по кругу, либо станет карго-культом. Лекарство дешёвое: Architecture Decision Record, короткий файл в репозитории рядом с кодом.
# ADR-014. Синхронный вызов вместо событий в оформлении заказа
Дата: 2026-07-16
Статус: принято (заменяет ADR-009)
## Контекст
Оформление заказа требует резерва товара на складе. Пиковая нагрузка — 400 rps,
продуктовое требование: пользователь узнаёт результат в течение 2 секунд.
Распределённой трассировки в проде пока нет.
## Решение
Синхронный HTTP-вызов складского сервиса с таймаутом 800 мс, двумя повторами
на идемпотентный запрос и предохранителем на 50% ошибок за минуту.
## Последствия
+ Простая диагностика: один запрос — одна трасса в логе.
+ Пользователь видит финальный статус сразу.
− Доступность оформления ограничена доступностью склада: 99.9% × 99.9%.
− При росте нагрузки выше 1000 rps решение пересматриваем (см. триггер ниже).
## Триггер пересмотра
Появление распределённой трассировки ИЛИ рост пиковой нагрузки выше 1000 rps.
Пять разделов, полстраницы, файл docs/adr/0014-*.md под контролем версий. Ценность не в шаблоне, а в двух вещах: явном разделе «Последствия» (он заставляет назвать цену) и триггере пересмотра (он превращает решение из догмы во временное). Формат предложил Майкл Найгард в заметке Documenting Architecture Decisions; коллекция вариантов — adr.github.io.
Подробно — «Архитектурные решения и ADR» и «ADR» с точки зрения того, как это написать так, чтобы читали.
7. Типичные ошибки
«Микросервисы, потому что так у больших». Netflix и Amazon пришли к сервисам, решая проблему сотен команд, которых у вас нет. Копируется решение, а не контекст — а шаблон без контекста не работает по определению. Проверка: назовите силу из шага 1, которая толкает вас к разделению. Не назвали — не делите.
Распределённый монолит. Сервисы разделены по сети, но выкатываются вместе, ходят в общую базу и падают все сразу. Худший из миров: цена распределённой системы заплачена, независимости нет. Диагностические признаки: релиз требует согласованного порядка выкатки; изменение схемы БД трогает три сервиса; отказ одного сервиса роняет остальные.
Слои ради слоёв. Пять уровней, каждый с собственным DTO, и на каждом — тождественное преобразование. Правило: уровень оправдан, если он что-то скрывает (заменяемая деталь, внешняя система, разная скорость изменения). Уровень, который только перекладывает поля, — это налог без услуги.
Контроллер на две тысячи строк. Формально MVC соблюдён, фактически модель анемична, а вся логика живёт в контроллере. Признак: чтобы понять бизнес-правило, приходится читать обработчик HTTP.
Событийная архитектура без наблюдаемости. Сообщения летают, а ответить на вопрос «почему заказ висит в PENDING» нельзя. Правило: сквозной идентификатор корреляции, очередь недоставленных сообщений и дашборд по лагу очереди должны появиться раньше первого события в проде, а не после первого инцидента.
Архитектура «по картинке». Диаграмма нарисована, ограничения не проверяются автоматически, через квартал код с ней не совпадает. Лечится тестами архитектуры (ArchUnit, import-linter, правила зависимостей в сборке) — см. главу «Качество в потоке».
Мини-итог
- Архитектура — это решения, которые дорого менять. Не выбрать их нельзя: невыбранное складывается само.
- Архитектурный шаблон описывает форму системы, паттерн проектирования — форму кода. Первый требует согласования с эксплуатацией, второй — только с ревьюером.
- Шаблон не бывает «хорошим»: он выгоден по одним атрибутам качества и убыточен по другим. Сначала назовите атрибуты, потом выбирайте.
- Семь базовых форм: уровни (логика), ярусы (физика), каналы и фильтры (поток данных), клиент-сервер (асимметрия ролей), MVC (состояние против отрисовки), события (развязка во времени), микросервисы (независимое развёртывание).
- Процедура: монолит по умолчанию → назвать силу, толкающую к разделению → проверить готовность эксплуатации → делить по одному куску.
- Единственная настоящая причина микросервисов — независимый релизный цикл. Всё остальное дешевле получить модульным монолитом.
- Каждое значимое решение — в ADR с разделом «Последствия» и триггером пересмотра.
Источники
- Bass, Clements, Kazman. Software Architecture in Practice, 4th ed. — источник схемы «контекст / задача / решение» и разбора атрибутов качества.
- Richards, Ford. Fundamentals of Software Architecture — стили, характеристики и способ их сравнивать.
- Мартин Фаулер: Microservice Trade-Offs, MonolithFirst, GUI Architectures.
- Мелвин Конвей. How Do Committees Invent? — оригинал закона Конвея, 1968.
- Enterprise Integration Patterns — канонический каталог стыков, включая каналы и фильтры.
- Майкл Найгард. Documenting Architecture Decisions и adr.github.io.
Что дальше
Форма системы выбрана и записана. Дальше — про инструмент, без которого ни одно архитектурное решение не доживает до второй недели: историю изменений, в которой видно, что и почему поменялось.