Архитектурный паттерн – фундамент разработки любого масштабного проекта. От его выбора зависит успешное развитие и поддержка бэкенд-систем. Традиционный шаблон MVC (Model-View-Controller) долгое время считался оптимальным выбором для разработки веб-приложений.
Современные вариации MVC включают механизмы внедрения зависимостей (IoC, DI), благодаря чему можно значительно усложнить и расширить его функционал. Однако даже сложные MVC-структуры имеют ограничения, особенно при масштабировании и поддержке растущей бизнес-логики.
Несмотря на то, что современные MVC-фреймворки позволяют реализовывать сложную архитектуру, базовый MVC-подход (без каких-либо дополнений) многие команды используют до сих пор.
В этой статье мы сделаем небольшой экскурс в эволюцию архитектурных подходов – от классического шаблона MVC, популярного на начальных стадиях разработки, до более современных решений, таких как SOA, DDD, Modular Monolith и микросервисы.
Наша цель – показать, как переход от одной архитектуры к другой может решить проблемы поддержки, тестирования и масштабируемости. А также дать рекомендации по выбору оптимального решения в зависимости от требований проекта.
Проблемы традиционного MVC

Главные преимущества MVC – скорость разработки и низкий порог вхождения. Это простой и понятный шаблон, который можно быстро освоить и внедрить в проект. Однако в долгосрочной перспективе эти преимущества могут повлечь за собой дополнительные временные затраты ввиду сложности поддержки. Они связаны с распределённостью бизнес-логики по контроллерам, ростом проекта, трудностями с тестированием и дублированием логики. Особенно это проявляется при отсутствии модульных тестов и невозможности переиспользовать код в разных частях приложения.

Традиционно, такие крупные проекты выглядят как набор огромных контроллеров. Бизнес-логика либо не переиспользуется, либо выносится в статичные функции или домен-модели, что вызывает стресс у разработчиков во время изменения и доработки любого компонента из-за невозможности предсказать потенциальные проблемы.
Но ничто не стоит на месте, и сегодня мы можем работать с современными реализациями MVC в фреймворках, которые поддерживают следующие возможности:
Интеграция IoC и DI – современные MVC-фреймворки (например, ASP.NET MVC, Laravel, Spring MVC) предлагают встроенные контейнеры для внедрения зависимостей, что повышает модульность и облегчает тестирование;
Чёткое разделение представлений и контроллеров – использование View Models, шаблонов и сервисов позволяет структурировать взаимодействие между слоями;
Расширяемость за счёт дополнительных слоёв – при необходимости MVC можно дополнить слоями бизнес-логики, доменной модели и инфраструктуры, что делает архитектуру более гибкой.
Однако, несмотря на видимые улучшения, остаются и ограничения:
Распыление бизнес-логики – даже при наличии сервисного слоя и репозиториев, логика бывает распределена между контроллерами, моделями и другими компонентами, что усложняет её поддержку;
Сложности масштабирования – в ходе роста проекта управление зависимостями и изменение функционала могут приводить к запутанности кода, что требует частого рефакторинга;
Ограничения в тестировании – несмотря на использование DI, тесное взаимодействие между слоями иногда приводит к необходимости проводить преимущественно интеграционные тесты, а не модульные.
Теперь, когда мы знаем про основную проблему, рассмотрим поэтапное развитие проекта, чтобы понять, когда может возникнуть необходимость в изменении архитектуры приложения. А также реально ли предугадать скорый переход на другие паттерны и подготовиться к нему.
Этапы эволюции архитектуры
По мере роста проекта в нем появляются новые функции, больше данных и пользователей. Чтобы система не становилась слишком сложной и неудобной, её архитектура должна развиваться.
Допустим, мы начали свой проект с ранее описанного MVC и запустили MVP-версию, которая быстро стала успешной. Клиент решает дорабатывать продукт, и с течением времени, мы начинаем сталкиваться со сложностями в поддержке и изменении существующей или новой логики. В таких случаях можно рассмотреть переход на другую архитектуру, чтобы сократить время на разработку и уменьшить количество багов.
Рассмотрим варианты, которые, на мой взгляд, лучше всего подходят для быстрорастущих продуктов.
Переход к Domain-Driven Design (DDD)

Это методология моделирования фокусируется на глубоком понимании предметной области. В DDD основной акцент сделан на создании «обогащённой» доменной модели и использовании таких концепций, как ограниченные контексты (Bounded Contexts), агрегаты, сущности и value objects. DDD помогает структурировать бизнес-логику внутри приложения, но не предписывает конкретную стратегию развертывания или взаимодействия между компонентами.
К преимуществам и особенностям перехода на DDD относятся следующие характеристики:
Снижение сложности за счет четкого описания предметной области. У каждого домена есть область ответственности, и его взаимодействие ограничено определёнными границами (Bounded Contexts);
Логика изменения и добавления новых функций легко вписывается в уже существующие доменные модели, если они правильно структурированы;
Внесение изменений в логику требует глубокого понимания доменных сущностей и их взаимосвязей;
Если система построена с использованием DDD, то изменения вписываются в существующий контекст, минимизируя риск появления хаотичной логики;
Добавление новых функций происходит через создание новых доменов или расширение текущих контекстов.
Переход на Modular Monolith

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

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


Главное преимущество такого подхода – возможность разбить приложение на независимые сервисы, каждый из которых отвечает за конкретную функцию. Например, отправку почты или обработку платежей. Это обеспечивает высокую гибкость, возможность масштабирования и независимое обновление каждого компонента.
Однако в этом случае у вас должна быть продуманная инфраструктура для оркестрации, мониторинга и управления межсервисным взаимодействием.
Микросервисная архитектура и SOA (Service-oriented architecture) говорят об одном и том же, но разными словами. Оба направлены на распределение системы на отдельные компоненты, но различаются в подходах, масштабах и применении.

При переходе на микросервисы или SOA мы получаем возможность настроить каждый компонент под конкретные задачи независимо от остальной системы. Например, отдельный сервис обработки данных может быть оптимизирован для работы с высоконагруженными потоками, в то время как сервис аналитики может использовать сложные алгоритмы.
Автономно работающие компоненты минимизируют влияние изменений в них работу всех системы. В модульном монолите или DDD границы между модулями (например, Bounded Contexts) более концептуальны, и изменения в одном модуле могут затронуть связанные части.
Возможность выбора различных языков для каждого компонента позволяет оптимизировать работу всех системы. В одном проекте можно использовать Python для обработки данных, Go – для высокопроизводительных сервисов, Node.js – для быстрого прототипирования.
Нагрузка на систему не однородна и, благодаря возможности масштабирования отдельных компонентов, можно эффективно управлять ресурсами и экономить на инфраструктуре.
Все преимущества перехода собраны в таблице:

Оба подхода имеют свои сильные стороны, и выбор между ними зависит от потребностей вашей системы. Таблица с сопоставлением их аспектов поможет определиться с нужной архитектурой:

Практический опыт и примеры из проектов
На своем опыте я столкнулся с двумя значимыми проектами, каждый из которых в определенный момент столкнулся с необходимостью изменений. Проблемы архитектуры и технический долг вынудили команды задуматься о рефакторинге и переходе на другую архитектуру. Хотя путь был весьма тернистым.
Кейс 1: переход на современную MVC-структуру
Средний по масштабу проект, где смена архитектуры заняла три месяца и была выполнена всего двумя разработчиками. Основной задачей стала полная переработка кода приложения, с целью привести его в соответствие с современной MVC-структурой и апдейтом фреймворка до Laravel 8.0. До переноса проект был на Laravel 5.8, где использовалась классическая модель: REST API с логикой, встроенной в модели, и перегруженные контроллеры, отвечавшие не только за маршрутизацию, но и за бизнес-логику. Попытки переиспользования кода сводились к вынесению логики в хэлперы, что со временем привело к избыточной связанности.
Основные сложности перехода:
Баги после перераспределения логики – исправление ошибок, появившихся вследствие изменения архитектурных принципов;
Давление со стороны бизнеса – ожидание более быстрого процесса, что осложняло итерации и отладку;
Разделение логики по слоям и сервисам – необходимость минимизировать связанность и достичь атомарности.
Самой серьезной проблемой стало разделение бизнес-логики. За годы разработки код писали разные команды, что привело к накоплению дублирующихся решений и сложности переиспользовать его. Логика могла встречаться на различных уровнях приложения – от middleware до моделей, что затрудняло рефакторинг. В процессе выявлялись новые кейсы, требующие расширения или модификации уже существующих решений, которые порождали цепочку переписывания сервисов и тестов. Иногда это приводило к необходимости разделить один сервис на несколько из-за его разрастания на 1-2 тысячи строк. Это влияло на взаимодействие компонентов и требовало дополнительной отладки, включая решение проблем с бесконечными зависимостями в DI.
Несмотря на сложности, переход принес значительные преимущества. Улучшенная архитектура, тестовое покрытие и упрощенная поддержка позволили ускорить разработку новых функций. В целом, скорость разработки увеличилась примерно на 25%, а количество багов сократилось на 30%, что подтверждает эффективность проведенного рефакторинга.
Кейс 2: путь через компромиссы
Проект включал 450+ контроллеров и всего 90 сервисов. На первый взгляд, странное соотношение, которое можно объяснить большой численностью команд, использовавших разные архитектурные подходы. В результате обнаружить и учитывать все места хранения бизнес-логики было практически невозможно.
Проект столкнулся с проблемами: большое количество багов, сложность поддержки, масштабирования и оптимизации. Эти ограничения вынуждали искать новые решения. Но ситуация оказалась сложнее: 100+ разработчиков, более 5 команд и отдельные группы менеджеров, аналитиков, архитекторов, DevOps и других специалистов участвовали в проекте. Консенсус между всеми этими группами был недостижим. Переход от идеи до начала реальных работ занял около полугода. Бизнес, как известно, неохотно выделяет ресурсы на исправление технического долга.
Вместо полного перехода к микросервисной архитектуре был принят компромисс: отдельные команды начали переводить свои компоненты в микросервисы. Существующий функционал на монолите продолжал использоваться, но интеграция с новыми микросервисами обеспечивалась через базы данных, Redis, Prometeus и новый API микросервисов. Это решение требовало значительных усилий со стороны DevOps-команды, поскольку опыт оркестрации микросервисов на новом для них языке Go был минимальным. Для минимизации рисков, в первую очередь, переводились компоненты с низкой нагрузкой.
Основные трудности в этом кейсе:
Обучение и адаптация – язык Go был новым для всех. Был нанят лидер разработки и организованы курсы переподготовки для PHP-разработчиков. Однако недостаток опыта привел к несогласованности данных между системами, крашам и потерям выручки;
Бутылочные горлышки – высокая нагрузка выявляла критические участки, требующие немедленного исправления;
Стресс команды – множество багов, нестабильность инфраструктуры и обилие проблем создавали ощущение приближающегося провала.
После сложного этапа стабилизации, включая исправление багов и оптимизацию узких мест, ситуация изменилась. Архитектура стала прозрачнее, разработка ускорилась, а тестирование позволило минимизировать ошибки. Свобода команд в разработке своих компонентов, возможность масштабирования отдельных сервисов и стандартизация API значительно повысили эффективность и независимость.
✍ Вывод. Переход на новую архитектуру неизбежно сопряжен с трудностями для всех — разработчиков, менеджеров, DevOps-команды и владельцев продукта. Тем не менее, результат оправдывает усилия: команды получают свободу работы над своими зонами ответственности, сервисы становятся более устойчивыми и масштабируемыми, а бизнес выигрывает за счет прозрачности и гибкости системы.
Рекомендации по выбору архитектурного паттерна
При выборе архитектуры стоит учитывать следующие критерии:
Тестируемость – архитектура должна позволять легко покрывать функциональные блоки модульными и интеграционными тестами;
Читаемость и поддерживаемость кода – структурированный код упрощает внесение изменений и снижает вероятность ошибок;
Переиспользование логики – разделение функционала на атомарные компоненты помогает избежать дублирования и ускоряет разработку новых функций;
Гибкость и масштабируемость – архитектура должна быть способна адаптироваться к изменяющимся требованиям проекта, позволяя поэтапно вводить новые возможности.
В контексте рассмотренных архитектур можно выявить следующие различия, которые могут помочь вам определиться:
Цель и область применения – DDD фокусируется на моделировании бизнес-логики, а SOA и Modular Monolith – на структурировании приложения с точки зрения развертывания и управления зависимостями;
Развертывание – SOA подразумевает независимое развертывание сервисов, в то время как Modular Monolith развёрнут как единое целое, несмотря на внутреннюю модульную структуру. DDD же не диктует способ развертывания;
Коммуникация – в SOA сервисы взаимодействуют по сети (например, через REST или SOAP), что влечёт за собой вопросы сетевой задержки, надёжности и безопасности. Modular Monolith не имеет таких проблем, так как модули вызывают друг друга напрямую внутри одного процесса;
Управление сложностью – DDD помогает управлять сложностью бизнес-логики через чёткое разделение доменной модели на контексты. SOA и Modular Monolith, напротив, структурируют систему с точки зрения технического разделения и развертывания.
Несмотря на схожесть в стремлении к разделению системы на логические части, эти подходы решают разные задачи: DDD – моделирование предметной области, SOA – организацию распределённой системы, а Modular Monolith – создание модульного, но цельного приложения.
Важно помнить, что выбор архитектурного паттерна зависит от специфики проекта, бюджета и долгосрочных целей заказчика. В некоторых случаях может быть оправдан старт с более простой архитектуры (MVC) с последующим переходом на DDD, если проект предполагает значительный рост и усложнение бизнес-логики.
Заключение
Современные реализации MVC с использованием IoC и DI могут быть сложными и эффективными для небольших и средних проектов. Однако при росте системы и усложнении бизнес-логики они могут столкнуться с проблемами масштабирования, фрагментации логики и сложности поддержки. Альтернативные архитектурные паттерны, такие как SOA, DDD, Modular Monolith и микросервисы, предлагают явное разделение обязанностей, изоляцию компонентов и гибкость, что значительно облегчает развитие и сопровождение крупных проектов.
При выборе архитектуры важно учитывать специфику проекта, требования к тестируемости, масштабируемости и будущему развитию системы. Каждая архитектура имеет свои преимущества, и правильный выбор часто зависит от баланса между скоростью разработки и долгосрочной поддерживаемостью продукта.
В следующей части мы поделимся кейсами для каждого варианта и сравним подходы на практике.
С вами была команда dev.family!