Инженерная практика Архитектурные шаблоны на практике: как выбрать форму системы
0%

Архитектурные шаблоны на практике: как выбрать форму системы

Архитектурные шаблоны на практике: как выбрать форму системы

Предыдущие четыре главы разбирали систему по частям: клиент, сервер, данные, интеграция. Эта глава — про то, как эти части складываются в форму целого, и почему форму приходится выбирать осознанно, даже если очень хочется «просто начать писать код».

Практическая проблема выглядит так. Команда садится за новый сервис, и через двадцать минут спор уже идёт не о продукте, а о словах: «давайте сразу микросервисы», «нет, монолит, но с чистой архитектурой», «а зачем нам слой репозиториев», «сделаем на событиях». Каждый участник спора прав по-своему, потому что каждый оптимизирует свой атрибут качества и не проговаривает какой. Задача этой главы — дать общий язык: какие вообще бывают формы систем, чем они отличаются на уровне последствий и по какой процедуре из них выбирают.

Это карта, а не учебник по каждому шаблону. На портале есть трек «Архитектурные шаблоны» из пятнадцати глав, где каждая форма разобрана вглубь — с кодом, схемами развёртывания и разбором отказов. Здесь мы проходим по всем формам сразу, чтобы вы видели пространство вариантов целиком, и в конце каждого разбора говорим, куда идти за деталями.

1. Архитектура — это множество решений, которые дорого менять

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

Отсюда следует важное: «не выбирать архитектуру» невозможно. Если вы не выбрали её сознательно, она сложилась сама — из первых десяти файлов, из привычек фреймворка, из того, кто первым добавил очередь. Разница только в том, знаете ли вы, какие решения приняты, и можете ли назвать их цену.

Решение Цена изменения через год
Имя переменной, структура функции Минуты, IDE переименует
Внутренняя структура модуля Часы-дни, покрыто тестами
Граница между сервисами Недели, миграция данных и контрактов
Модель согласованности данных Месяцы, переписывается бизнес-логика
Способ развёртывания и релизный цикл Квартал, меняется процесс всей команды

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

Архитектурный шаблон — это документированное решение повторяющейся задачи в определённом контексте, вместе с описанием последствий. Три слова тут одинаково важны: задача (что именно болит), контекст (при каких условиях это болит) и последствия (чем вы заплатите за лекарство). Шаблон без контекста и последствий превращается в моду, а мода в архитектуре стоит дороже всего.

2. Шаблон, стиль и паттерн проектирования: три разных слова

Эти три термина путают постоянно, и путаница дорого обходится в разговоре.

Архитектурный стиль — самый широкий уровень: набор ограничений на форму системы. «Клиент-сервер», «многоуровневый», «событийный» — это стили. Стиль не говорит, из каких компонентов состоит ваша система; он говорит, какие связи в ней разрешены.

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

Паттерн проектирования (GoF: «Приёмы объектно-ориентированного проектирования») — про форму кода внутри одного компонента: Наблюдатель, Стратегия, Фабрика, Декоратор. Он не влияет на развёртывание, не меняет сетевые границы и не требует согласования с эксплуатацией.

Архитектурный шаблон Паттерн проектирования
Единица Компонент, сервис, узел Класс, функция, модуль
Что определяет Форму системы Форму кода
Кто участвует в решении Команда, эксплуатация, иногда бизнес Автор кода и ревьюер
Цена ошибки Недели-месяцы Часы
Виден ли снаружи Да: в задержках, отказах, релизах Нет

Простая проверка: если для смены решения нужно менять схему развёртывания или контракт между командами — это архитектура. Если достаточно рефакторинга в одном репозитории под зелёными тестами — это паттерн проектирования. Разбор самих паттернов — в треке «Паттерны проектирования»; шаблоны и паттерны не конкурируют, а живут на разных этажах: событийная архитектура снаружи прекрасно уживается с Наблюдателем внутри обработчика.

3. Настоящий критерий выбора — атрибуты качества

Функциональные требования почти никогда не диктуют архитектуру: «пользователь оформляет заказ» реализуется и в монолите, и в двадцати сервисах, и в скрипте на Bash. Форму системы определяют нефункциональные требования — атрибуты качества.

Рабочий минимум, который стоит проговорить перед любым выбором:

  • Модифицируемость — сколько мест придётся тронуть для типичного изменения. Главный вопрос: какие изменения ожидаются часто?
  • Производительность — задержка и пропускная способность на реальном профиле нагрузки.
  • Доступность — что происходит при отказе части системы, какой процент запросов выживает.
  • Тестируемость — можно ли проверить логику без поднятия половины инфраструктуры.
  • Наблюдаемость — можно ли по логам и трассам ответить, где именно сломалось.
  • Стоимость владения — счёт за инфраструктуру плюс человеко-часы на поддержку.
  • Скорость поставки — сколько времени проходит от коммита до продакшна и сколько команд надо синхронизировать.

Ключевая мысль: шаблон не бывает «хорошим». Он выгоден по одним атрибутам и убыточен по другим, всегда. Многоуровневая архитектура покупает понятность ценой задержки; микросервисы покупают независимость релизов ценой сетевой сложности; событийная архитектура покупает развязку ценой наблюдаемости. Ваша задача — не найти лучший шаблон, а понять, какие атрибуты для вас критичны, и заплатить именно там, где не больно. Как формулировать такие требования измеримо (не «система должна быть быстрой», а «p99 ниже 300 мс при 1000 rps») — в главе «Нефункциональные требования».

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

4. Семь шаблонов в едином формате

Дальше — семь разборов по одной и той же схеме: Контекст → Задача → Решение → Чем платишь → Когда применять → Глубже. Схема взята из книги Басса, Клементса и Кацмана «Software Architecture in Practice» и хороша именно тем, что не даёт спрятать раздел «чем платишь».

4.1. Многоуровневая архитектура (layered)

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

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

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

Три уточнения, которые чаще всего теряются:

  1. Уровень — техническое разделение, а не доменное. «Все контроллеры вместе, все репозитории вместе» — это про роль в обработке запроса, а не про предметную область. Отсюда типичная боль: изменение одной бизнес-функции трогает четыре уровня в четырёх каталогах.
  2. Однонаправленность нужно проверять автоматически, иначе она разрушается за квартал. Линтеры импортов, ArchUnit, import-linter — дешевле любого код-ревью.
  3. Каждый уровень — это ещё один переход, копирование данных и маппинг 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 с разделом «Последствия» и триггером пересмотра.

Источники

Что дальше

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

Контроль версий как инженерная практика

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов