Comments 10
Классический MVC ‒ это паттерн уровня «UI», поэтому он просто не обсуждает, где именно должны лежать:
• «чистый» домен,
• сценарии-use-cases (application services / интеракторы),
• инфраструктурные детали.
Если вы пользуетесь только терминологией MVC, то у вас действительно нет «правильной полки» для этих слоёв. Приходится либо:
Набивать всё внутрь одной из трёх букв (чаще всего в «M», получая «fat model» и God-objects), либо
Добавлять к MVC другие архитектурные идеи: Layered Architecture, Hexagonal/Ports & Adapters, Clean Architecture, DDD и т. п
Ниже — практическая «карта роста» типового веб-проекта, начинающегося с «простого MVC» и постепенно эволюционирующего в более слоистую/портовую архитектуру. Для каждого этапа указано:
• характерные признаки;
• проблемы, которые обычно всплывают;
• «сигналы тревоги», говорящие, что пора менять архитектуру;
• подготовительные шаги, позволяющие перейти на следующий уровень без боли.
Примеры будут в терминах .NET/Laravel/Spring, но логика универсальна.
Этап 0. Прототип / MVP
Состояние
• 1–3 разработчика, 10–30 экранов.
• Контроллеры вызывают ActiveRecord-модели или ORM-Entity напрямую.
• В коде много «action + SQL + if-else».
Почему работает
• Максимальная скорость поставки фич.
• Минимальный «церемониал» ‒ одна папка controllers, одна models и views.
Когда начинает болеть
• Дублирующаяся валидация и расчёты («а если поменять налог, надо искать 6 мест»).
• Появляются интеграции: e-mail, очередь, платежи.
• Юнит-тесты отсутствуют или покрывают только хелперы.
Подготовка к следующему этапу
Подключите DI-контейнер (во всех трёх упомянутых фреймворках он уже есть).
Соглашение «один контроллер = только оркестрация + маппинг DTO/VM».
Вынесите общие алгоритмы в отдельные классы/модули, даже если они пока «static util».
Этап 1. «Сервисный слой» (Application layer)
Состояние
• Контроллеры тоньше: каждое действие вызывает сервис (interactor/use-case).
• Сервисы знают о транзакциях, репозиториях, интеграциях.
• Модели всё ещё ActiveRecord/Entity; доменных правил немного.
Проблемы, которые обычно всплывают
• «God-Service»: один класс на 500 строк, 20 методов.
• Начинаются циклические зависимости между сервисами.
• Доменная логика распылена: часть в Entity callback’ах, часть в сервисах.
Сигналы «пора дальше»
• Появились разные контексты (Billing, Inventory, CRM) с пересекающимися таблицами.
• Бизнес просит вариативности: «если тип клиента = Premium, считать по другим правилам».
• Тесты сервисов становятся хрупкими — нужно мокать пол-приложения.
Подготовительные шаги
Ввести репозитории как интерфейсы (IOrderRepository), а ORM-классы — реализация.
Отделить «DTO на входе/выходе» (ViewModel, Request/Response) от «Entity» — убираем утечки MVC внутрь домена.
Начать писать юнит-тесты на сервисы с моками репозиториев (DI уже есть).
Этап 2. «Чистый домен» (DDD light / Hexagonal)
Состояние
• Появился отдельный пакет/namespace domain: Entity, ValueObject, Aggregate, Domain-Service.
• Классы ORM превращены в мапперы (Data Mapper) или скрыты за репозиторием.
• Сервисный слой вызывает домен, но не наоборот.
• Порты/адаптеры («инфра») реализуют хранение + интеграции.
Что начинает болеть
• Разрастание портов: десятки интерфейсов, сложно следить за реализацией.
• Нужны транзакции сквозь несколько агрегатов, а домен о БД не знает.
• Build-time/compile-time увеличивается; девопс-процессы усложняются.
Сигналы «пора дробить»
• Команда > 10-15 человек, параллельно 3–4 стрима фич.
• Разным командам мешают изменения БД-схемы друг друга.
• Требуется разная «скорость» компонентов (заказчик хочет отдельный SLA для платежей).
Подготовка
Ограниченные контексты (Bounded Context) формально закрепляются: отдельные модули, схемы или БД.
В CI — контекстные тест-пайплайны (каждый пакет можно билдить отдельно).
Ввести events/pub-sub внутри монолита (Domain Events) — пригодится при миграции в сервисы.
Этап 3. Модульный монолит / Микросервисы
Возможные пути
A) Модульный монолит (single deployable, но строгие границы кода/данных).
B) Стратегия Strangler Fig: постепенно «выкусываем» контексты наружу как сервисы.
Критические проблемы, которые решаются
• Независимое масштабирование (нужно 10 экземпляров payment-svc, но 1 экземпляр admin-UI).
• Изоляция отказов: падение отчётности не должно валить платёжный API.
• Раздельные технологические стеки (ML-модуль на Python, остальное на Java/Rust).
Как подготовиться заранее
Асинхронные контракты (Kafka/Rabbit/Service Bus) вместо прямых вызовов между контекстами.
Версионирование API на уровне application layer, не только public REST.
Автотесты контрактов (Consumer-Driven Contracts).
Набор DevOps-артефактов: docker-образы, helm-чарты, наблюдаемость (tracing, metrics).
Можно ли «предугадать» момент перехода?
На 100 % — нет: бизнес-приоритеты меняются. Но можно поставить «сигнальные маяки».
Метрики-триггеры
• Число строк в одном классе/файле > 300 / метод > 50 строк.
• Средний PR затрагивает > n файлов (n=15-20) — признак сильных связей.
• Время сборки/тестов > 10-15 минут — тормозит цикл обратной связи.
• MTTR инцидента ↑, потому что сложно локализовать владелец кода.
Процессные индикаторы
• «Bus factor» = 1 (есть модули, которые знает один человек).
• Фичевой команде приходится ждать другую команду для релиза.
• P1-баг фиксится правкой кода в 5 местах («shotgun surgery»).
Как действовать
Выписывать такие метрики в инженерный roadmap.
Закладывать чистку архитектуры в OKR/Sprint Goal (tech-budget).
Использовать RFC-процесс для обсуждения крупных рефакторингов.
Делать эволюцию инкрементально (Branch by Abstraction, Feature Toggle).
Что, если проект «застрял» где-то посередине?
• Не пытайтесь «переписать всё заново».
• Найдите самый болезненный use-case и сделайте его по новой схеме внутри текущего кода (Vertical Slice).
• Поставьте метрики до/после, чтобы доказать пользу.
• Постепенно распространяйте практику.
Выводы
MVC — лишь уровень UI, сам по себе он не обязывает вас держать домен, use-cases и инфраструктуру в порядке.
Архитектурная эволюция практически неизбежна; подготовиться можно, дисциплинированно вводя DI, service layer, репозитории, events и autotests задолго до «точки боли».
Следите за количественными и процессными сигналами: они подскажут, когда «пора».
Переход делается итеративно: вертикальные срезы, feature toggles, strangler-pattern.
Так вы избежите ситуаций, когда «огромный контроллер» или «fat model» становятся неприступной крепостью, и сможете менять технологический стек или архитектурный стиль без остановки бизнеса.
Не раскрыты минусы микросервисов. А их столько что в большинстве случаев монолит в разы лучше, удобнее и производительнее.
Как минимум разбить продукт хорошо с нуля практически нереально.
Резко растёт нагрузка на сеть.
Растет потребление ресурсов, поскольку условный сервер вам придётся поднимать кратно количеству сервисов чтоб они могли общаться друг с другом
Растет число точек отказа и можно легко поиметь chain dos, когда один навернувшийся сервис перегружает соседа, который перегружает соседа, который... и так далее.
Есть огромные проблемы с тестированием, по сути с микросервисами вы отрезаете себе локальное тестирование без геморроя с интеграцией (нельзя например просто поднять весь спринг или фласк прямо в юнитах и прошерстить проект от и до).
Есть большие проблемы с транзакциями и их откатом
Есть проблемы со сбором/удалением данных под какой-нибудь gdpr, когда вам надо пройтись по всем сервисам и ничего не забыть
Есть ситуации когда бизнес-логика резко меняется и ранее отдельные сервисы оказываются жёстко связаны и начинают кидать друг в друга запросами без остановки.
Несмотря на схожесть в стремлении к разделению системы на логические части, эти подходы решают разные задачи: DDD – моделирование предметной области, SOA – организацию распределённой системы, а Modular Monolith – создание модульного, но цельного приложения.
Интересная мысль. Можно построить 3 независимых координатных оси.
Три самых частых вопроса, из-за которых мы спорим об архитектуре, можно представить как три простых «ползунка».
Что именно у вас сложное?
• Алгоритмы (формулы, графы, математика) ←────────→ Бизнес-правила (статусы, тарифы, роли)
‑ Алгоритмы сложны → нужны быстрые функции, минимум слоёв. DDD мало помогает.
‑ Правила сложны → нужен DDD или хотя бы «богатая» доменная модель.
Насколько мелко режем приложение при деплое?
Монолит ── Modular Monolith ── Микросервисы
‑ Чем правее, тем проще каждой команде жить отдельно, но сложнее DevOps и наблюдаемость. И общая управляемость может быть сложнее.
Как куски системы общаются?
Централизованно («шина», ESB) ←────────→ Децентрализованно (REST, события)
‑ Слева один «узел связи», всё жёстко стандартизировано. Это полезно для интеграции крупных приложений от разных поставщиков.
‑ Справа сервисы сами владеют своими API, много мелких интеграций.
Впрочем, обычно речь идёт отрезать ли один кусок или не отрезать. То есть одна часть системы может быть по одному шаблону, другая часть про другому
Извините, заспамил немножко.
Интереснее отслеживать не пути развития архитектур, а изменение целей и приоритетов. А изменение архитектур как следствие.
Ниже — четыре характерные области корпоративной ИТ, где за последние 15-20 лет существенно поменялись именно ЦЕЛИ. После каждой пары «было → стало» в скобках кратко указываю, какой архитектурный сдвиг за этим последовал.
───────────────
Интеграция корпоративных систем
Было (≈2000-е)
• Соединить десяток COTS-коробок.
• Гарантировать ACID-транзакции и аудит.
• Минимизировать изменения внутри купленного ПО.
→ Архитектура: EAI/ESB, SOAP, централизованный Canonical Data Model.
Стало (≈2020-е)
• Коробочные продукты потеряли популярность и ценность
• Стало важно время вывода продукта или сервиса на рынок. Обеспечить поток событий «почти-real-time» для цифровых сервисов.
• Дать продуктовым командам подключаться без change-board-а ESB.
• Сохранять регуляторный аудит, но децентрализовано.
→ Архитектура: фасады-адаптеры вокруг коробок, Kafka/Pulsar, CDC/Outbox, API-gateway; ESB превращается в «легаси-мост», а не центр логики.
───────────────
Клиентские и партнёрские digital-сервисы
Было
• Статичный веб-сайт / личный кабинет; релиз — раз в квартал.
• Основная метрика — «работает и не падает».
→ Монолитный back-end (J2EE, .NET), серверный HTML, LAMP.
Стало
• «Feature velocity» — релизы ежедневно.
• Омниканал: мобильное, веб, партнёрские API, IoT.
• Персонализация, A/B, глобальное масштабирование.
→ Архитектура: SPA + micro-frontends, BFF, cloud-native микросервисы, serverless функции, CI/CD «push-to-prod».
Данные и аналитика
Было
• Месячные/квартальные отчёты для руководства.
• Допустимы задержки в часы-сутки.
→ EDW на Oracle/Teradata, nightly ETL, star-schema.
Стало
• Решения в режиме «сейчас»: antifraud, рекомендации, динамические цены.
• Быстрая итерация ML-моделей, эксперименты on-line.
→ Архитектура: стрим-пайплайны (Flink, Spark Streaming), data lake → lakehouse, feature store, MLOps-конвейер; основной транспорт данных — события, а не CSV-дамп.
Эксплуатация и инфраструктура
Было
• Главная цель — «не ломать»; изменения — через change-meeting раз в месяц.
• Ручное развёртывание, вертикальное масштабирование.
→ ITIL, physical/VM-кластер, hand-made scripts.
Стало
• Два KPI: частота деплоя и MTTR.
• Нужны облачная эластичность и надёжность «by design».
→ Архитектура: DevOps + GitOps, IaC, контейнеры, Kubernetes, service mesh, policy-as-code, full-stack observability (tracing, SLO).
Итого
Во всех областях сместилась «верхнеуровневая» цель:
• с «стабильность любой ценой» → к «быстрые изменения при сохранении надёжности».
Архитектурные приёмы (от централизованной шины до событийных и облачных платформ) являются лишь ответом на эту новую комбинацию скоростей, масштабов и регуляторных ограничений.
В следующей части мы поделимся кейсами для каждого варианта и сравним подходы на практике
Больше конкретных кейсов, пожалуйста. А то получается разговор двух LLM философов пустых болтунов
Бггг
Не рассмотрена гибридная архитектура, когда стандартная логика реализована в монолите, а хитрые/сложные/заковыристые или часто меняющиеся функции вынесены в сервисы.
Эволюция архитектурных паттернов в бэкенд-разработке: от MVC к микросервисам