Перейти к основному содержимому

6.11. Архитектурная практика

Разработчику Архитектору Аналитику

Введение: что такое архитектурная практика

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

Архитектурная практика не сводится к выбору конкретного шаблона или фреймворка. Она включает в себя:

  • осознанное проведение границ ответственности между частями системы;
  • управление зависимостями и связностью (coupling);
  • контроль за когезией (cohesion) на уровне доменов и сервисов;
  • баланс между гибкостью и стабильностью;
  • предвидение точек роста и потенциальных узких мест;
  • выбор стратегии эволюции: развитие существующей архитектуры или её пересмотр.

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

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


1. Компоненты монолита. Двухзвенная и трёхзвенная архитектура

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

Классический монолит состоит из следующих логических компонентов:

  • Пользовательский интерфейс (UI) — слой представления, реализующий взаимодействие с конечным пользователем. Может быть веб-интерфейсом (HTML/CSS/JS), десктопным приложением, мобильным клиентом.
  • Бизнес-логика (application layer) — ядро системы, где реализуются правила предметной области, оркестрация операций, транзакционные гарантии.
  • Доступ к данным (data access layer) — компоненты, отвечающие за взаимодействие с постоянным хранилищем: ORM, репозитории, DAO, клиенты баз данных.

Несмотря на единую точку развёртывания, даже в монолите возможна внутренняя модульность — например, через пространства имён, отдельные библиотеки, чёткие правила инверсии зависимостей (Dependency Inversion Principle). Однако границы между модулями в монолите остаются логическими, а не физическими, и их нарушение приводит к накоплению технического долга.

Двухзвенная архитектура

Двухзвенная (2-tier) архитектура — упрощённая модель, где клиентское приложение напрямую взаимодействует с СУБД. Например: десктопное приложение, подключающееся к PostgreSQL по сети. В такой схеме бизнес-логика либо отсутствует, либо частично размещается на клиенте, либо — в триггерах и хранимых процедурах. Это подход типичен для небольших информационных систем конца 1990‑х — начала 2000‑х.

Преимущества:

  • Минимальная инфраструктура;
  • Простота развёртывания (один клиент, один сервер БД);
  • Низкая сетевая латентность между клиентом и БД.

Недостатки:

  • Отсутствие централизованной бизнес-логики, что затрудняет поддержку согласованности при множестве клиентов;
  • Высокая связность клиентского кода с моделью данных;
  • Сложности с безопасностью: клиент оперирует учётной записью с прямыми правами на БД;
  • Плохая масштабируемость: при росте числа клиентов нагрузка ложится на СУБД без возможности горизонтального масштабирования логики.

Трёхзвенная архитектура

Трёхзвенная (3-tier) архитектура — естественное развитие двухзвенной. Здесь явно выделяется сервер приложений как отдельный слой между клиентом и базой данных. Клиент общается только с сервером приложений, тот, в свою очередь, взаимодействует с БД через строго контролируемые интерфейсы.

Структурно:

  1. Presentation tier — клиент (тонкий или толстый, об этом ниже);
  2. Application tier — сервер приложений: веб-сервер, API-шлюз, бизнес-сервисы;
  3. Data tier — СУБД, кэши, файловые хранилища.

Трёхзвенная архитектура стала доминирующей в эпоху веб-приложений и остаётся актуальной для большинства enterprise-систем. Она позволяет:

  • Централизовать бизнес-правила;
  • Внедрять аутентификацию, авторизацию, аудит, логирование на уровне сервера;
  • Гибко масштабировать логику независимо от хранилища;
  • Поддерживать несколько клиентов (веб, мобильный, интеграции) через единый контракт.

Однако даже трёхзвенная система может быть монолитной — если все три слоя поставляются в одном исполняемом артефакте и масштабируются синхронно.


2. Плюсы и минусы монолитной архитектуры

Монолит остаётся обоснованным выбором при определённых условиях.

Преимущества монолита

  • Простота разработки и отладки на ранних этапах. Единая кодовая база, единая IDE-сессия, возможность пошаговой отладки от UI до БД — всё это ускоряет итерации в стартовой фазе проекта.
  • Единая транзакционная граница. Любая операция, затрагивающая несколько модулей, может быть обёрнута в одну ACID-транзакцию. Это упрощает обеспечение консистентности без сложных механизмов компенсации.
  • Минимальная сетевая сложность. Отсутствие межсервисного взаимодействия означает отсутствие задержек, десериализации, обработки таймаутов, idempotency, circuit breakers.
  • Упрощённый CI/CD. Один pipeline, один артефакт, одна операция деплоя — меньше точек отказа в процессе поставки.
  • Низкие накладные расходы на инфраструктуру. Один процесс, один контейнер, один экземпляр БД — проще администрировать, дешевле в эксплуатации.

Недостатки монолита

  • Связность (coupling) растёт нелинейно. По мере роста команды и функционала границы между модулями стираются: появляются циклические зависимости, прямые вызовы между «слоями», дублирование логики. Архитектура деградирует в «большой комок грязи» (Big Ball of Mud).
  • Масштабирование «всё или ничего». Нельзя масштабировать только нагруженный компонент — придётся реплицировать весь монолит, даже если 90 % его функционала простаивает.
  • Ограниченная технологическая гибкость. Смена языка, фреймворка или СУБД затрагивает всю систему. Эксперименты с новыми технологиями в отдельных модулях невозможны.
  • Долгая сборка и деплой. При размере кодовой базы в сотни тысяч строк время компиляции, тестирования и развёртывания может измеряться десятками минут, что замедляет цикл обратной связи.
  • Риск единой точки отказа. Падение одного компонента (например, из-за memory leak в модуле отчётов) выводит из строя всю систему.
  • Сложность вовлечения новых разработчиков. Требуется понимание всей системы, даже если человек работает над узкой функцией.

Таким образом, монолит наиболее эффективен в условиях:

  • небольшой команды (до 5–7 человек);
  • узкого и чётко определённого домена;
  • отсутствия требований к частому масштабированию или изоляции компонентов.

3. Понятие software architect и архитектурной ответственности

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

Ключевые обязанности архитектора:

  • Формулировка архитектурного видения — краткое, понятное описание того, как система должна быть устроена, чтобы соответствовать стратегическим целям.
  • Определение ключевых нефункциональных требований (NFR): latency, throughput, availability, security, evolvability — и их количественная спецификация (SLI/SLO).
  • Выбор архитектурного стиля и паттернов, соответствующих контексту.
  • Введение и поддержка архитектурных ограничений (constraints): запреты на прямые вызовы между модулями, требования к интерфейсам, правила именования, подходы к обработке ошибок.
  • Архитектурные ревью — систематическая проверка соответствия решений видению.
  • Обучение команды: объяснение почему принято то или иное решение, а не только что делать.

Архитектор не обязан писать код ежедневно, но должен быть способен сделать это — иначе его решения рискуют стать оторванными от реальности. Идеально, когда архитектор участвует в написании критически важных компонентов (например, ядра доменной логики или шлюза агрегации), чтобы сохранять «тактильную» связь с системой.


4. Архитектурные паттерны (не путать с GoF)

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

Некоторые ключевые архитектурные паттерны:

  • Слоистая архитектура (Layered Architecture) — классическая трёх- или четырёхслойная структура (presentation, business, persistence, database). Подходит для приложений с чётким разделением ответственности и умеренным темпом изменений.
  • Шина событий (Event Bus / Event-Driven Architecture) — компоненты взаимодействуют через асинхронные события, публикуемые в шину (Kafka, RabbitMQ, NATS). Поддерживает слабую связность, но требует дисциплины в моделировании событий и обработке ошибок.
  • Ориентированная на команды и запросы (CQRS) — разделение операций записи и чтения на разные модели и, зачастую, разные физические компоненты.
  • Микросервисная архитектура — декомпозиция по границам домена с физическим разделением развёртывания и данных.
  • Serverless / FaaS — делегирование управления инфраструктурой облачному провайдеру, фокус на функциях как единицах развёртывания.
  • Service Mesh — вынос логики коммуникации (retry, circuit breaking, mTLS) во внешний слой (sidecar-proxy), освобождая бизнес-код от инфраструктурной сложности.

Выбор архитектурного паттерна — это стратегическое решение. Например, переход на микросервисы без изменений в процессы команды (CI/CD, мониторинг, эксплуатация) часто ведёт к распределённому монолиту — системе, которая объединяет недостатки обоих миров.


5. TL;DR как инструмент архитектурного мышления

TL;DR («Too Long; Didn’t Read») в профессиональной среде — дисциплина архитектурного синтеза. Архитектор обязан уметь выразить суть решения в 2–4 предложения, доступных как техническому руководителю, так и нетехническому заказчику. Это требует:

  • чёткого разделения проблемы и решения;
  • отказа от жаргона без пояснения;
  • фокуса на последствиях, а не на механизмах.

Пример корректного TL;DR для перехода на CQRS:

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

Некорректный вариант:

«Будем использовать CQRS с Event Sourcing и Kafka, это модно и правильно» — отсутствует проблема, последствия и контекст.

TL;DR — это стартовая точка для дискуссии. Он должен вызывать конкретные вопросы: Какой уровень задержки допустим при чтении? Как обрабатывать конфликты при одновременной записи? Как обеспечить аудит? Если TL;DR не порождает уточняющих вопросов — он либо слишком общий, либо скрывает сложность.


6. Шардирование: управление масштабом данных

Шардирование — стратегия горизонтального разделения единого логического набора данных на несколько физических сегментов (шардов), каждый из которых содержит подмножество записей. Цель — преодолеть ограничения одной инстанции СУБД по объёму данных, числу операций в секунду или пропускной способности дисковой подсистемы.

Ключевые принципы

  • Шардирующий ключ — атрибут (или комбинация), по которому происходит распределение записей. Примеры: user_id, tenant_id, region_code. Ключ должен обеспечивать равномерное распределение нагрузки и локальность данных — запросы по одному пользователю/клиенту должны попадать в один шард.
  • Прозрачность для приложения — идеально, если логика шардирования инкапсулирована в слое доступа к данным (например, через библиотеку вроде Hibernate Shards или Vitess). Однако на практике часто приходится вносить изменения в бизнес-логику: например, запрещать JOIN’ы между сущностями из разных шардов.
  • Ребалансировка — процесс перераспределения данных при изменении числа шардов. Вручную это дорого и рискованно; автоматизированные решения (Vitess, Citus, YugabyteDB) включают координационные узлы и фоновые задачи репликации.

Типы шардирования

  • Диапазонное — шарды выделяются по диапазонам значений ключа (0–999, 1000–1999). Просто в реализации, но подвержено дисбалансу при неравномерном распределении ключей (например, если большинство пользователей — из одного региона).
  • Хеширование — ключ хешируется (например, CRC32), и по остатку деления определяется шард. Обеспечивает равномерное распределение, но усложняет диапазонные запросы (WHERE created_at BETWEEN …).
  • Географическое — шарды привязаны к регионам для минимизации latency и соблюдения требований локализации данных (например, GDPR). Требует явного управления репликацией между регионами.

Ограничения и риски

  • Глобальные операции становятся сложными. Агрегация (COUNT, SUM), сортировка, JOIN’ы — требуют координации между шардами, что снижает производительность и повышает latency.
  • Усложнение администрирования. Резервное копирование, восстановление, миграции схемы — должны выполняться для каждого шарда отдельно или с координацией.
  • Жёсткая привязка к ключу. Изменение шардирующего ключа — операция уровня переписывания системы.

Шардирование оправдано, когда:

  • объём данных превышает 1–2 ТБ на инстанс;
  • write/read throughput близок к пределу одного узла;
  • требования к latency не позволяют использовать вертикальное масштабирование.

В остальных случаях предпочтительны более простые стратегии: партиционирование внутри СУБД, read replicas, кэширование.


7. MV* паттерны: разделение ответственности на уровне представления

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

MVC (Model-View-Controller)

  • Model — состояние и бизнес-логика, не зависящие от UI.
  • View — пассивный элемент отображения, который оповещается об изменениях модели (часто через observer).
  • Controller — обрабатывает входные события (клик, ввод), изменяет модель, инициирует обновление View.

Особенность: View имеет прямой доступ к Model для чтения (но не для записи). Контроллер — thin, так как основная логика в модели. Типично для серверных веб-фреймворков (Ruby on Rails, Spring MVC), где View — это шаблон, отрендеренный на сервере.

MVP (Model-View-Presenter)

  • Presenter берёт на себя всю логику взаимодействия: получает события от View, читает/пишет в Model, явно обновляет View через интерфейс (например, IView.UpdateUserName()).
  • View полностью пассивна — не имеет доступа к Model, не знает о бизнес-правилах. Это позволяет легко писать unit-тесты для Presenter’а.

Распространён в desktop- и mobile-разработке (Android до Jetpack Compose, WinForms), где важна тестируемость UI-логики.

MVVM (Model-View-ViewModel)

  • ViewModelпредставление состояния для View. Он предоставляет observable-свойства (например, userName: string, isLoading: boolean).
  • View подписывается на изменения ViewModel (через data binding), автоматически обновляя интерфейс.

Ключевое отличие: поток данных — реактивный, однонаправленный. MVVM наиболее эффективен в средах с мощными binding-фреймворками (WPF, SwiftUI, Angular, Vue, React с MobX/Zustand).

Выбор паттерна

Решение зависит от:

  • платформы (сервер vs клиент, наличие binding);
  • требований к тестируемости;
  • командных компетенций.

Например, в SPA на React чистый MVC не реализуем — компонентная модель ближе к MVVM (state → render), но с императивными элементами (useEffect как Presenter). Важно соблюдение принципа: изменение UI не должно требовать изменения бизнес-логики, и наоборот.


8. CQRS: разделение команд и запросов

Command Query Responsibility Segregation — принцип, утверждающий: операции, изменяющие состояние (команды), следует строго отделять от операций, только читающих состояние (запросы). В классической реализации это означает:

  • две разные модели данных:
    • Write model — нормализованная, транзакционная, ориентированная на целостность;
    • Read model — денормализованная, оптимизированная под конкретные UI-сценарии, часто хранящаяся в NoSQL или колоночной БД.
  • два разных API:
    • POST /orders — команда, создающая заказ;
    • GET /order-summary/{id} — запрос, возвращающий агрегированные данные для карточки заказа.

Почему это работает

  • Оптимизация под нагрузку. Write-сторона масштабируется по числу транзакций (ACID), read-сторона — по количеству запросов (возможно, с кэшированием и CDN).
  • Эволюционная гибкость. Можно менять read-модель без пересборки write-логики — например, добавить новый отчёт, просто создав новую проекцию.
  • Изоляция сложности. Валидация, агрегация, инварианты — остаются в write-модели, не засоряя read-сторону.

Проблемы и компромиссы

  • Eventual consistency. Read-модель обновляется асинхронно — между записью и отображением может быть задержка (мс–с). Требуется продумать UX: показывать «сохранено», но «данные обновятся через несколько секунд».
  • Сложность синхронизации. Проекции могут «отставать»; нужны механизмы повторной обработки событий, идемпотентности, отката.
  • Удвоение кода. Две модели, две схемы, два набора тестов.

CQRS не требует Event Sourcing (хотя часто используется с ним). Можно применять CQRS поверх реляционной БД: write — в PostgreSQL, read — в проекции на Redis или Elasticsearch, обновляемые триггерами или фоновыми job’ами.

CQRS оправдан, когда:

  • read/write нагрузки сильно различаются по объёму или структуре;
  • требуется высокая масштабируемость интерфейсов;
  • доменная модель сложна, и попытки «одной модели на всё» приводят к компромиссам.

Для простых CRUD-приложений CQRS — избыточность, создающая технический долг.


9. Декомпозиция монолита: модульность и микросервисы

Декомпозиция — средство снижения сложности. Она возможна в двух плоскостях: логической (модули в едином артефакте) и физической (независимые процессы, развёртывания, данные).

Декомпозиция на модули

Модуль — автономная единица кода с чётким контрактом, минимальными внешними зависимостями и высокой внутренней когезией. В .NET — отдельная сборка (Project.dll), в Java — JAR-модуль, в Python — пакет с явными __init__.py и py.typed.

Принципы модульной декомпозиции:

  • Инверсия зависимостей. Высокоуровневые модули не зависят от низкоуровневых — оба зависят от абстракций (интерфейсов, DTO).
  • Замкнутость по модификации (OCP). Изменение одного модуля не требует изменения других — только при расширении контракта.
  • Явное управление видимостью. Публичные API модуля — минимальны; внутренние классы скрыты (internal в C#, package-private в Java).
  • Автономная сборка и тестирование. Модуль можно собрать и прогнать unit-тесты без остальной системы.

Инструменты поддержки:

  • в .NET — InternalsVisibleTo, PrivateAssets, анализ зависимостей через dotnet-dependency-analyzer;
  • в Java — JPMS (Java Platform Module System), module-info.java;
  • в Python — mypy с strict mode, pydeps для визуализации связей.

Модульный монолит — переходная, но устойчивая архитектура. Он сохраняет преимущества монолита (единая транзакция, простой деплой), но устраняет основную его слабость — неуправляемую связность.

Декомпозиция на микросервисы

Микросервис — независимо развёртываемая, слабосвязанная единица, ответственная за конкретную бизнес-возможность в рамках ограниченного контекста (bounded context).

Критерии «настоящего» микросервиса:

  • Собственная граница данных (отдельная БД или схема, запрет прямого доступа извне);
  • Собственный жизненный цикл (CI/CD, версионирование, откат);
  • Собственный мониторинг и логирование (tracing ID, метрики по сервису);
  • Контрактный интерфейс (API, события) — единственный способ взаимодействия.

Декомпозиция на микросервисы должна начинаться с анализа домена (см. DDD ниже). Частая ошибка — разрезание по техническим признакам («сервис пользователей», «сервис отчётов»), что приводит к тесной связности и «распределённой транзакции» через REST.


10. Управление данными в микросервисной архитектуре

В микросервисной архитектуре каждый сервис владеет своими данными и имеет исключительное право на их изменение. Это принцип data ownership, лежащий в основе слабой связности. Прямой доступ к БД другого сервиса — архитектурное нарушение, ведущее к скрытым зависимостям и невозможности независимой эволюции.

Проблема распределённых транзакций

Классическая ACID-транзакция, охватывающая несколько сервисов, невозможна без введения централизованного координатора (2PC/XA), что противоречит целям микросервисов: отказоустойчивости и автономности. Вместо этого применяются компенсирующие стратегии:

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

    1. ReserveInventory → в случае ошибки CancelReservation;
    2. ChargePayment → в случае ошибки Refund;
    3. ShipOrder → в случае ошибки CancelShipment.
      Saga может быть хореографией (каждый сервис реагирует на события) или оркестрацией (центральный orchestrator управляет потоком).
  • Outbox-паттерн — обеспечение атомарности «изменение данных + публикация события». При обновлении записи в БД в ту же транзакцию добавляется запись во внутреннюю таблицу outbox. Фоновый процесс забирает события из outbox и отправляет в шину (Kafka, RabbitMQ). Это исключает потерю событий при сбое после коммита БД, но до отправки.

  • Event Sourcing — хранение последовательности событий, его породивших. Состояние восстанавливается проигрыванием лога. Позволяет легко строить проекции, аудит, откаты — но требует дисциплины в моделировании событий и управления эволюцией схемы.

Выбор хранилища

Микросервисы могут использовать разные типы БД — в зависимости от характера данных:

  • Реляционные (PostgreSQL, MySQL) — для транзакционных операций с сильной целостностью;
  • Документные (MongoDB, Couchbase) — для гибких, иерархических сущностей с редкими JOIN’ами;
  • Ключ-значение (Redis, DynamoDB) — для кэширования, сессий, временных данных;
  • Графовые (Neo4j) — для отношений, где важны связи (социальные графы, маршрутизация);
  • Временные ряды (InfluxDB, TimescaleDB) — для метрик, логов, IoT-данных.

Важно: использование нескольких типов БД (polyglot persistence) оправдано только при явной необходимости. Добавление нового типа — это долгосрочное обязательство по обучению команды, мониторингу и резервному копированию.

Гарантии консистентности

В распределённых системах приходится выбирать между:

  • Сильной консистентностью (linearizability) — дорого, плохо масштабируется;
  • Eventual consistency — данные сойдутся со временем, дешевле и устойчивее.

Архитектор должен явно определить, для каких операций допустима eventual consistency, а где нужна транзакционная изоляция. Например:

  • Баланс счёта — strong consistency (через Saga с блокировками);
  • Отображение рекомендаций — eventual consistency (данные обновляются раз в минуту).

11. Толстый и тонкий клиент: распределение логики

Термины «толстый» и «тонкий» клиент относятся к тому, где выполняется оркестрация бизнес-логики — на стороне клиента или сервера.

Тонкий клиент (thin client)

  • Выполняет только отображение и сбор ввода.
  • Вся логика — на сервере: валидация, агрегация, преобразование данных.
  • Коммуникация — через высокоуровневые API (например, GET /dashboard-data, возвращающий готовый DTO для UI).
  • Преимущества:
    — единая точка контроля логики;
    — простота поддержки нескольких клиентов (веб, мобильный, интеграции);
    — безопасность: клиент не видит внутренние структуры данных.
  • Недостатки:
    — высокая нагрузка на сервер при сложных UI;
    — latency при множестве микровызовов («chatty API»);
    — сложность кэширования на клиенте.

Толстый клиент (thick client)

  • Содержит значительную часть логики: маршрутизацию, кэширование, предварительную валидацию, локальное состояние.
  • Сервер предоставляет низкоуровневые интерфейсы (например, GET /users?id=123, GET /orders?userId=123).
  • Клиент сам оркестрирует вызовы, агрегирует данные, управляет состоянием (например, через Redux или Zustand).
  • Преимущества:
    — отзывчивый интерфейс (меньше round-trip’ов);
    — автономность (offline-режим);
    — гибкость UI без изменения сервера.
  • Недостатки:
    — дублирование логики между клиентами;
    — сложность тестирования и поддержки;
    — риск утечки бизнес-логики в клиентский код.

Современный гибрид: BFF (Backend for Frontend)

Наиболее сбалансированный подход — выделение специализированных серверных шлюзов под каждый тип клиента:

  • web-bff — агрегирует данные для веб-интерфейса, кэширует, адаптирует под роутинг SPA;
  • mobile-bff — сжимает данные, оптимизирует под медленные сети, управляет версионированием API;
  • integration-bff — предоставляет контракты для внешних партнёров.

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


12. Коммуникация микросервисов

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

Синхронная коммуникация

  • REST/HTTP — де-факто стандарт. Прост в отладке, поддерживается повсеместно.
    Риски:
    — каскадные отказы при таймаутах;
    — сложность обеспечения idempotency (повторный вызов не должен создавать дубль);
    — неявные зависимости через цепочки вызовов (A → B → C → D).

  • gRPC — бинарный, контрактный, с поддержкой streaming и строгой типизацией через proto-файлы.
    Преимущества:
    — высокая производительность;
    — генерация клиентов и серверов;
    — встроенный deadline propagation.
    Недостатки:
    — сложность отладки без инструментов (нужен BloomRPC или подобное);
    — слабая поддержка в браузере (требуется gRPC-Web + proxy).

Асинхронная коммуникация

  • Событийная (event-driven) — сервис публикует событие (OrderCreated), другие — подписываются.
    Ключевые требования:
    — идемпотентность обработчиков (событие может прийти дважды);
    — управление порядком (Kafka гарантирует порядок в партиции, но не глобально);
    — отслеживание состояния обработки (checkpointing, dead-letter queues).

  • Командная (command-driven) — явная отправка команды (ApproveOrder) конкретному получателю. Часто реализуется поверх очередей (RabbitMQ с routing keys).

Гибридные схемы

На практике используется комбинация:

  • Запросы — синхронно (REST/gRPC), с жёсткими SLA по latency;
  • Изменения состояния — асинхронно (события), с eventual consistency.

Не каждая операция требует ответа. Например, отправка уведомления — это фоновое событие.


13. Построение пользовательского интерфейса в распределённых системах

UI — это архитектурный элемент, подверженный тем же требованиям к масштабируемости и эволюции.

Проблемы интеграции UI-компонентов

При декомпозиции по доменам возникает вопрос: как собрать единую страницу из данных нескольких сервисов?

  • Микрофронтенды (Micro Frontends) — подход, при котором независимые команды поставляют фрагменты UI (например, через Web Components, Module Federation в Webpack 5 или single-spa).
    Преимущества:
    — независимое развёртывание частей интерфейса;
    — выбор технологий на уровне фичи (React для аналитики, Vue для админки).
    Риски:
    — дублирование зависимостей (множество копий React);
    — сложность обеспечения единого UX/UI-kit’а;
    — проблемы с общей аутентификацией и состоянием.

  • BFF для агрегации — более контролируемый подход. BFF вызывает несколько сервисов, собирает DTO, применяет преобразования (например, маппинг UserDTO + OrderDTO → DashboardViewModel) и отдаёт клиенту структуру, идеально подходящую под текущий рендер.

Единый UX без единой кодовой базы

Чтобы избежать «лоскутного одеяла», вводятся:

  • Design System — библиотека компонентов, токенов, гайдлайнов;
  • Контракты интерфейсов — API, UI-контракты: форматы метаданных, правила локализации, семантика уведомлений;
  • Сквозная идентификация — единый tracing ID, передаваемый от UI до БД.

14. Повышение отказоустойчивости

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

Основные принципы

  • Изоляция (bulkheading) — ограничение распространения сбоев. Пример: пулы соединений к БД выделяются по сервисам; падение одного сервиса не исчерпывает все соединения.
  • Таймауты — все внешние вызовы должны иметь явный deadline. Бесконечное ожидание — худший режим отказа.
  • Circuit Breaker — при превышении порога ошибок вызовы временно блокируются, возвращается fallback-ответ. Библиотеки: Polly (.NET), Resilience4j (Java), Istio (на уровне service mesh).
  • Грациозная деградация (graceful degradation) — при частичном отказе система продолжает работать в ограниченном режиме.
    Пример:
    — недоступен сервис рекомендаций → показываем популярные товары из кэша;
    — недоступна аналитика → логируем в локальный буфер, отправляем позже.

Fallback-стратегии

  • Кэш как fallback — при ошибке чтения из БД — вернуть устаревшие данные из Redis.
  • Статический ответ — «Сейчас высокая нагрузка, повторите через 5 минут».
  • Очередь с отложенной обработкой — запись в локальный store + фоновая синхронизация.

fallback не должен быть «заглушкой». Он должен быть тестируемым и мониторинг-дружелюбным (метрика fallback_triggered{service="orders"}).


15. Проведение архитектурных границ

Архитектурная граница — это контролируемая точка взаимодействия между компонентами, в которой явно определены:

  • контракт (входные/выходные данные, семантика ошибок);
  • допустимые зависимости (что может вызывать что);
  • стратегия изменения (как вносить breaking changes без катастрофы).

Границы проводят по следующим критериям:

По скорости изменения

Компоненты, меняющиеся с разной частотой, должны быть разделены. Пример:

  • ядро доменной логики — редко (раз в квартал);
  • интеграции с внешними системами — часто (еженедельно);
  • UI-слои — постоянно (ежедневно).

Если всё собрано в один модуль — любая правка интеграции требует полного регрессионного тестирования ядра.

По уровню критичности

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

  • обработка платежей — строгий аудит, двухфакторная валидация, отдельная БД;
  • публичный каталог товаров — кэшируемый, read-only, без персональных данных.

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

По принадлежности к команде

Граница должна совпадать с организационной (Conway’s Law). Если две команды работают над одним модулем — возникнет координационный оверхед, merge-конфликты, компромиссы в архитектуре. Разделение по границам ответственности снижает transaction cost взаимодействия.

Инструменты фиксации границ

  • Контрактные тесты (consumer-driven contracts) — Pact, Spring Cloud Contract. Каждый потребитель фиксирует, какие поля и поведения он ожидает; провайдер проверяет совместимость до деплоя.
  • Статический анализ зависимостей — ArchUnit (Java), NDepend (.NET), Deptrac (PHP). Запрещает вызовы между пакетами/слоями на уровне CI.
  • Чёткие правила именования — например, *.Core, *.Integration, *.UI — сразу видно, к какому слою относится компонент.

Граница — проходной пункт с досмотром. Чем строже контроль на границе, тем свободнее внутренняя реализация.


16. Повышение уровня абстракции интерфейсов

Интерфейс — это публичный контракт между системами. Его качество определяет долгосрочную жизнеспособность архитектуры.

Принципы проектирования контрактов

  • Семантическая насыщенность — имя операции должно отражать намерение, а не механизм.
    Плохо: POST /api/v1/update — что обновляется? как?
    Хорошо: PATCH /orders/{id}/cancel — ясное намерение, идемпотентно.

  • Стабильность через версионирование — в заголовках (Accept: application/vnd.myapp.order+json;version=2). Позволяет гибко управлять совместимостью.

  • Гипермедиа как двигатель состояния (HATEOAS) — ответ содержит данные и возможные следующие действия:

    {
    "id": "ORD-123",
    "status": "confirmed",
    "_links": {
    "cancel": { "href": "/orders/ORD-123/cancel", "method": "POST" },
    "ship": { "href": "/orders/ORD-123/ship", "method": "POST" }
    }
    }

    Это делает клиент устойчивым к изменениям в workflow: если ship недоступен, ссылка просто отсутствует.

  • Явная обработка ошибок409 Conflict с телом:

    {
    "code": "INSUFFICIENT_STOCK",
    "message": "Товара недостаточно на складе",
    "details": { "available": 5, "requested": 10 }
    }

    Это позволяет клиенту принимать осмысленные решения (предложить альтернативу, запросить подтверждение).

Отказ от «CRUD-мышления»

Многие API проектируются как тонкая обёртка над таблицами: GET /users, POST /users, PUT /users/123. Это приводит к:

  • утечке внутренней структуры (например, password_hash в ответе);
  • невозможности выразить бизнес-операции (transferFunds, mergeAccounts);
  • жёсткой связности клиентов с моделью данных.

Вместо этого — операционно-ориентированные интерфейсы, отражающие действия предметной области.


17. Выделение доменов: Domain-Driven Design как архитектурный инструмент

DDD — набор техник для работы со сложностью в предметной области. Его ценность для архитектора — в структурировании мышления.

Ограниченный контекст (Bounded Context)

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

  • В контексте Billing: Customer — плательщик с реквизитами;
  • В контексте Support: Customer — человек с историей обращений.

Смешение контекстов ведёт к «размытой» модели, где Customer — гигантский класс на 200 полей.

Карта контекстов (Context Mapping)

Описывает, как контексты взаимодействуют:

  • Partnership — совместная эволюция (редко);
  • Shared Kernel — общая подсистема с жёсткой синхронизацией (риск);
  • Customer-Supplier — один контекст зависит от другого, но поставщик учитывает потребности;
  • Conformist — потребитель принимает модель поставщика как есть (например, внешний API);
  • Anticorruption Layer (ACL) — адаптер, защищающий внутреннюю модель от «токсичной» внешней.
    Пример: интеграция с устаревшей системой — ACL преобразует её XML-выход в доменные события.

ACL — один из самых мощных инструментов архитектора. Он позволяет:

  • сохранить чистоту внутренней модели;
  • изолировать изменения в legacy-системе;
  • постепенно заменять внешние зависимости.

Стратегическое проектирование

DDD предлагает начинать с:

  1. Глоссария (Ubiquitous Language) — совместное определение терминов с экспертами предметной области;
  2. Выявления агрегатов — корневых сущностей с транзакционными границами (например, Order + OrderLines);
  3. Разделения на Core Domain / Supporting Subdomains / Generic Subdomains — чтобы сосредоточить инновации там, где они дают конкурентное преимущество.

DDD особенно полезен при:

  • высокой сложности бизнес-логики;
  • наличии нескольких команд;
  • долгосрочной поддержке системы (5+ лет).

18. Оптимизация кодовой базы: не ради скорости, а ради сопровождаемости

Архитектор часто сталкивается с требованием «ускорить систему». Но реальная оптимизация — это снижение когнитивной нагрузки при чтении и изменении кода.

Принципы читаемой архитектуры

  • Явность превыше краткости
    Плохо: var r = svc.P(u, f)
    Хорошо: var report = reportingService.GenerateProfitReport(user, filter)

  • Отсутствие «магии»
    Автоматическая инъекция зависимостей — хорошо; автоматическая генерация SQL по именам методов — плохо (непредсказуемо, трудно отлаживать).

  • Единый стиль во всей базе
    Единые правила:
    — именование методов (Create, Update, Delete, GetByX);
    — обработка ошибок (исключения vs Result<T>);
    — логирование (структурное, с ключевыми полями).

  • Документирование намерения, а не реализации
    Комментарий // Using Dijkstra for shortest path бесполезен — это видно из кода.
    Комментарий // Must use Dijkstra (not A*) because edge weights can be negative — ценен.

Инструменты поддержки

  • Архитектурные линтеры — проверяют соответствие слоёв, отсутствие циклических зависимостей.
  • Автоматическая документация API — OpenAPI/Swagger, но с описаниями, а не только схемами.
  • Кодовые соглашения в CI — запрет на TODO без issue, обязательные код-ревью для изменений в Core.

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


19. Проектирование для архитектурной целостности и масштабируемости

Завершающий синтез: как спроектировать систему, которая не «развалится» через три года?

Архитектурная целостность — это дисциплина

  • Архитектурный совет (Architecture Review Board) — регулярные сессии (раз в 2–4 недели), где рассматриваются:
    • новые границы;
    • нарушения контрактов;
    • технический долг с оценкой ROI ремонта.
  • Архитектурные решения как документы (ADR) — фиксация почему было принято решение, какие альтернативы отклонены и почему. Хранятся в репозитории, версионируются.
  • «Архитектурный долг» в бэклоге — явные задачи: «Выделить ACL для ERP-интеграции», «Перейти с REST на gRPC для внутренних вызовов».

Масштабируемость — это про измеримость

Масштабируемость не проверяется «на глаз». Она выражается в:

  • SLI (Service Level Indicators) — latency, error rate, throughput;
  • SLO (Service Level Objectives) — целевые значения (например, p99 latency < 200 мс при 1000 RPS);
  • SLA (Service Level Agreements) — обязательства перед клиентами.

Архитектор определяет граничные условия:

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

Стратегия эволюции

  • Стратегия «Strangler Fig» — постепенное замещение монолита: новые фичи — в новых сервисах, старые — постепенно переносятся. Обратный прокси (например, на основе Envoy) маршрутизирует запросы.
  • Флаги возможностей (feature flags) — позволяют включать/выключать логику без деплоя, проводить A/B-тесты архитектурных решений.
  • Архитектурные «пробы» — выделение временного сервиса на 2–3 месяца для проверки гипотезы (например, «Event Sourcing сократит время отладки на 30 %»). По итогу — решение: масштабировать или свернуть.

20. Архитектура игровых приложений: Unity и Unreal Engine как среды проектирования

Разработка игр — это проектирование реактивных, stateful-систем с жёсткими временными ограничениями (обычно 16.6 мс на кадр при 60 FPS). Архитектурные решения здесь определяются стабильностью фреймрейта, предсказуемостью поведения и возможностью итеративного дизайна.

Unity и Unreal — это целостные среды, включающие редактор, сценографию, систему ассетов, сериализацию и runtime. Архитектура должна учитывать эту двойственность: дизайн-тайм vs runtime, редактор vs билд, data-driven vs code-driven.

20.1. Общие архитектурные вызовы в играх

  • Жёсткие временные рамки. Любая операция (физика, рендер, AI) должна укладываться в бюджет кадра. Блокирующие вызовы, неожиданные GC-паузы, непредсказуемые аллокации — недопустимы.
  • Состояние как центр. Игровой мир — это гигантский граф объектов с изменяющимися свойствами. Сохранение/загрузка, откат, сетевая репликация — всё строится на управлении этим состоянием.
  • Итеративность дизайна. Геймдизайнеры меняют правила в процессе тестирования. Архитектура должна позволять вносить изменения без перекомпиляции (через скрипты, конфиги, инспектор).
  • Смешанная ответственность. Один объект (например, Player) часто объединяет физику, анимацию, UI, звук, сохранение — что в enterprise-системах было бы нарушением SRP. Компромисс здесь осознан: ради производительности и удобства работы в редакторе.

20.2. Unity: архитектурные стратегии

Unity поощряет компонентную модель (ECS — Entity Component System — опциональна, но рекомендуется для сложных проектов). Однако архитектор должен осознанно выбирать уровень абстракции.

Уровни проектирования в Unity:
  1. MonoBehaviour-уровень (наивный подход)
    Прямое наследование MonoBehaviour, логика в Update(), OnCollisionEnter() и т.п.
    Риски:
    — тесная связь с движком (нельзя unit-тестировать без PlayMode);
    — трудности с рефакторингом (логика размазана по Start, Update, FixedUpdate);
    — проблемы с жизненным циклом (активация/деактивация объектов вызывает неожиданные побочные эффекты).

  2. Сервисный уровень (Application Layer)
    Вынос бизнес-логики в обычные C#-классы (GameStateManager, InventorySystem), которые инкапсулируют правила и не знают о GameObject. MonoBehaviour становится адаптером между движком и доменом:

    public class PlayerController : MonoBehaviour {
    private PlayerLogic _logic;

    void Awake() => _logic = new PlayerLogic();
    void Update() => _logic.Update(Input.GetAxis("Horizontal"));
    void OnCollisionEnter(Collision c) => _logic.OnHit(c.relativeVelocity);
    }

    Преимущества:
    — тестируемость;
    — возможность переиспользовать логику в серверной части (например, для authoritative server в мультиплеере);
    — чёткое разделение «что» и «как».

  3. ECS / DOTS (Data-Oriented Tech Stack)
    Для high-performance сценариев (тысячи сущностей, симуляции):

    • Entities — идентификаторы без поведения;
    • Components — чистые данные (struct);
    • Systems — обрабатывают группы компонентов (JobComponentSystem).
      Архитектурные последствия:
      — отказ от OOP в пользу data-oriented design;
      — сложность отладки (отсутствие привычных ссылок, объектов);
      — необходимость явного управления жизненным циклом (EntityManager).
Ключевые архитектурные практики в Unity:
  • Событийная система поверх UnityEvent или C# events — избегать прямых вызовов GetComponent<Other>().DoSomething() между объектами. Вместо этого — event Action<Vector3> OnPositionChanged.
  • Инъекция зависимостей (Zenject, VContainer) — позволяет собирать иерархии объектов без FindObjectOfType, поддерживать тестируемость.
  • Data-Oriented конфигурации — ScriptableObject как immutable-конфиги (WeaponStats, LevelConfig). Они сериализуются в редакторе, проверяются статически, легко версионируются.
  • Feature Flags для баланса — выносить в настроечные ассеты, управляемые через Addressables.

20.3. Unreal Engine: архитектурные особенности

Unreal сочетает C++ и Blueprints, что создаёт уникальный контекст: архитектура — это договорённость между программистами и дизайнерами.

Основные уровни:
  1. C++ Core Layer
    Критически важная логика: сетевая репликация, сохранение, математика, low-level взаимодействие. Пишется на C++ с использованием UClass, UPROPERTY, RPC-макросов.
    Принципы:
    UCLASS() — только для того, что должно сериализоваться или реплицироваться;
    — бизнес-логику выносить в обычные классы (FGameRules), не наследующие UObject;
    — избегать «Blueprint-нативных» методов (UFUNCTION(BlueprintCallable)) без необходимости — они создают жёсткую связь.

  2. Blueprint Layer
    Визуальное программирование для геймплея, UI, анимации. Архитектор должен определить:
    что может делаться в Blueprints, а что — только в C++;
    — как обеспечить совместимость при изменении интерфейсов (например, через Version у UBlueprint и миграции).

  3. Data Assets (Data-Driven Design)
    UDataAsset — аналог ScriptableObject в Unity. Примеры: UCharacterClassData, ULevelRewardTable.
    Преимущества:
    — геймдизайнеры меняют баланс без компиляции;
    — единая точка истины для параметров;
    — поддержка локализации через FText.

Архитектурные паттерны в Unreal:
  • Gameplay Ability System (GAS) — официальный фреймворк для сложных взаимодействий (умения, эффекты, стейты). Он вводит:
    Ability — конкретное действие (FireGun, Dash);
    AttributeSet — параметры (Health, Stamina);
    GameplayEffect — модификаторы (+10 Damage for 5 sec).
    GAS — это архитектурный каркас, а не просто плагин. Его внедрение требует дисциплины, но даёт:
    — предсказуемую репликацию;
    — отладку через Gameplay Debugger;
    — композицию эффектов без «спагетти-кода».

  • Subsystems — глобальные сервисы (UGameStateSubsystem, UAnalyticsSubsystem). Позволяют избежать синглтонов и GetWorld()->GetSubsystem<T>().

  • Plugin-Based Architecture — вынесение функционала в плагины (OnlineSubsystem, ProceduralMeshPlugin). Это:
    — изоляция зависимостей;
    — возможность отключения функций под билд (Mobile vs PC);
    — чистый core проекта.


Важное замечание по кроссплатформенности:
В играх архитектурные границы часто совпадают с платформенными модулями. Например:

  • Core — логика, не зависящая от платформы;
  • Platform.Android / Platform.iOS — нативные вызовы;
  • Input.Legacy / Input.Enhanced — разные системы ввода.
    Такая структура позволяет легко добавлять поддержку новых ОС без пересборки всей игры.

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

Структура файловой системы — это видимое проявление архитектуры. Плохая организация — первый признак деградации: «а где у нас обработка заказов?», «почему этот файл в Utils, но он связан только с отчётами?».

Принцип: файловая структура должна отражать архитектурные границы, а не технические категории.

21.1. Анти-паттерны организации кода

  • По типам файлов

    /Models
    /Views
    /Controllers
    /Services
    /DTOs

    Проблема: чтобы внести фичу «оплата», нужно прыгать между папками. Нет локальности изменений.

  • По технологиям

    /Database
    /API
    /UI
    /Tests

    Смешивает слои и функционал. Код «экспорта в PDF» может быть в /Services, /Utils, /Jobs — неясно, где искать.

  • По историческим причинам
    /Legacy, /OldIntegration, /V2 — технический долг, закреплённый в файловой системе.

21.2. Feature-Based (Domain-Driven) структура

Код группируется по бизнес-возможностям или ограниченным контекстам:

/src
/Core # Ядро: доменные модели, правила (без зависимостей от фреймворков)
/Entities
/ValueObjects
/DomainEvents
/Specifications

/Application # Сценарии использования: команды, запросы, обработчики
/Orders
CreateOrderCommand.cs
CreateOrderHandler.cs
OrderDto.cs
/Payments
ProcessPaymentCommand.cs
...

/Infrastructure # Реализация: БД, интеграции, внешние сервисы
/Persistence
/EF # Entity Framework
/Dapper
/ExternalServices
/ERPClient
/NotificationService

/Presentation # UI-слои (может быть несколько)
/WebApi
/Controllers
/Swagger
/BlazorApp
/MobileApp (отдельный проект)

/Shared # Межконтекстные примитивы (осторожно!)
/Kernel
Result.cs
EntityId.cs
/Common
DateTimeProvider.cs

Преимущества:

  • Локальность изменений: фича «отмена заказа» — только в /Orders;
  • Ясность границ: Core не может ссылаться на Infrastructure;
  • Поддержка модульности: /Orders можно вынести в отдельный проект/репозиторий.

21.3. Правила именования и видимости

  • Интерфейсы и реализации — в одном файле или рядом:
    IOrderRepository.cs, OrderRepository_EF.cs, OrderRepository_Mock.cs.
  • Внутренние классыinternal, а не public; используют InternalsVisibleTo только для тестов.
  • Файлы — по одному классу (кроме вложенных, DTO, enums) — упрощает поиск и merge.

21.4. Инструменты поддержки структуры

  • .editorconfig — единые правила отступов, кодировки, окончаний строк.
  • Directory.Build.props (MSBuild) — централизованные настройки проектов (.NET).
  • deps.json / project-graph — визуализация зависимостей между модулями (NDepend, dotnet list dependencies).
  • Pre-commit hooks — запрет коммитов с TODO, непроверенных миграций, нарушений структуры.

21.5. Для игровых проектов — адаптированная структура

В Unity/Unreal файловая организация тесно связана с ассетами, поэтому используется гибрид:

/Assets
/Scripts
/Core
/Domain
PlayerState.cs
GameState.cs
/Systems
MovementSystem.cs
CombatSystem.cs

/Application
/Features
/Inventory
InventoryController.cs
ItemData.cs (ScriptableObject)

/Infrastructure
/Save
SaveService.cs
/Networking
PhotonAdapter.cs

/Plugins # Внешние SDK (не в /Scripts!)
/Resources # Только то, что требует Resources.Load (минимизировать)
/Addressables # Для динамической загрузки

В Unreal — через Content Folders и Plugins:

/Source
/MyGame.Core # C++ core
/MyGame.Gameplay # GAS, Abilities
/MyGame.UI # UMG, ViewModels

/Plugins
/Analytics
/LootboxSystem

Ключевой принцип: ассеты и код — единая система. Если WeaponData.asset используется только в CombatSystem, он должен находиться рядом (/Combat/WeaponData.asset), а не в /Data/Weapons.