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

Когда слои есть, а границ нет
В проекте появляются контроллеры, сервисы, репозитории и модели, но изменение одного правила всё равно требует правок в пяти папках. Обычно проблема не в том, что слоёв слишком мало, а в том, что у них нет устойчивых границ: контроллер решает, кому можно выдать скидку, репозиторий отправляет письмо, а сервис передаёт HTTP-ответ. Такая архитектура выглядит упорядоченной в дереве файлов, но не в поведении приложения.
Разберём типичные ошибки на нейтральной схеме: внешний слой принимает запрос, прикладной координирует действие, доменный содержит правила предметной области, инфраструктурный работает с базой и внешними системами. Это не рецепт для конкретного фреймворка и не инструкция по его API: схема применима к веб-приложениям на разных стеках. Версия языка или библиотеки здесь не определяет рекомендации.
Слой полезен не потому, что у него отдельная папка. Он полезен, если его можно изменить, не перенося вместе с ним чужую ответственность.
Четыре ошибки, которые стирают границы
Контроллер становится местом для бизнес-правил
Контроллер начинает проверять права, вычислять итоговую цену, менять статус заказа и решать, какую запись сохранить. Сначала это удобно: запрос проходит по одной функции. Потом правило нужно повторить в фоновой задаче или административном интерфейсе — и его копируют, потому что контроллер нельзя вызвать из другого сценария без HTTP-контекста.
Оставляйте во внешнем слое разбор запроса, проверку формата входных данных, передачу команды дальше и преобразование результата в HTTP-ответ. Решение вроде «нельзя отменить уже отправленный заказ» должно жить рядом с предметной логикой, а не зависеть от того, кто вызвал операцию: браузер, задача по расписанию или другой сервис.
Сервис превращается в свалку действий
Название вроде OrderService ничего не говорит о границе ответственности. Если класс создаёт заказ, считает скидку, формирует письмо, выбирает платёжного провайдера и записывает аудит, он не стал универсальным центром — он собрал несвязанные причины для изменений.
Проверка простая: назовите одно действие, которое этот компонент выполняет, и правило, за которое отвечает. Если для описания нужны слова «и ещё», разделите обязанности по сценариям или по смысловым правилам. Но не дробите каждую строку на отдельный класс: число файлов само по себе не показывает качество границ.
Репозиторий знает больше, чем должен
Репозиторий, который возвращает HTTP-ответы, отправляет уведомления или применяет бизнес-ограничения, смешивает доступ к данным с поведением приложения. Обратная крайность — интерфейс репозитория для каждой таблицы, хотя данные никогда не меняются независимо и отдельная абстракция ничего не упрощает.
Сначала определите, какую операцию приложению действительно нужно выполнить над данными: например, получить заказ для проверки или сохранить его новое состояние. Репозиторий может скрывать детали хранения, но не должен становиться местом для всех правил, связанных с сущностью. Если правило требует согласованного изменения нескольких записей, отдельно проверьте границы транзакции: их нельзя выводить только из названия класса.
Зависимости направлены не туда
Если доменная логика напрямую зависит от конкретной базы, очереди или веб-фреймворка, заменить инфраструктуру труднее — и тестировать правила отдельно тоже. Это не значит, что любой проект обязан вводить интерфейс для каждого внешнего вызова. Абстракция оправдана, когда она защищает важную границу или позволяет независимую замену, а не просто добавляет ещё один файл.
Проследите направление вызовов на одном сценарии. Запрос приходит извне, прикладной компонент запускает действие, а реализация доступа к базе подключается через границу, определённую потребностью приложения. Если предметное правило импортирует классы фреймворка или знает формат сетевого ответа провайдера, зависимость протекла внутрь.
Какой способ организации выбрать
Два распространённых варианта — складывать файлы по техническим слоям или группировать их вокруг функций продукта. Первый проще для небольшого приложения с несколькими сценариями. Второй помогает, когда команда развивает независимые части продукта, но требует договориться о границах модулей.
| Подход | Что получаете | Чем платите | Когда подходит |
|---|---|---|---|
| Папки по слоям | Понятные места для контроллеров, логики и доступа к данным | Код одного сценария оказывается разбросан по нескольким каталогам | Небольшой проект с ограниченным числом функций |
| Модули по функциям | Код сценария и его правила собраны ближе друг к другу | Нужны правила, чтобы модули не копировали общую логику и не зависели друг от друга напрямую | Несколько развиваемых независимо функций или команд |
| Смешанная схема | Внутри функциональных модулей сохраняются нужные технические границы | Нужно следить, чтобы структура не превратилась в одинаковые слои внутри каждого маленького модуля | Проект уже вырос из одной общей папки, но не требует отдельных сервисов |
Это не сравнение производительности: без измерений нельзя утверждать, что одна структура быстрее. Выбирайте по цене изменений. Если правка сценария регулярно затрагивает несвязанные модули, попробуйте группировку по функциям. Если команда не может найти, где устроен доступ к данным, названия слоёв могут оказаться полезнее.
Как проверить архитектуру на одном сценарии
- Выберите изменение с понятным результатом. Например, добавить правило запрета отмены заказа после передачи в доставку. Запишите, где оно сейчас реализовано и какие входные данные ему нужны.
- Проверьте повторное использование. Спросите, сможет ли то же правило сработать из административного интерфейса или фоновой задачи без запуска HTTP-контроллера. Если нет — логика, вероятно, расположена слишком близко к внешнему входу.
- Проследите зависимости. Найдите, что правило импортирует и какие компоненты вызывает. Зависимость от формата HTTP-ответа или конкретного клиента базы — сигнал проверить границу.
- Уточните владельца данных и транзакции. Если сценарий меняет несколько связанных записей, выясните, где обеспечивается атомарность и кто отвечает за откат. Не полагайтесь на случайный порядок вызовов.
- Сравните цену правки до и после. Считайте затронутые модули, дублирующиеся проверки и тесты, которые пришлось менять. Это не универсальная метрика качества, а наблюдение за конкретным изменением.
При повторной проверке должно быть понятно, где живёт правило, кто его запускает и какие компоненты можно заменить отдельно. Если ответ каждый раз зависит от конкретной команды или автора кода, сначала улучшите соглашения и тесты, а не добавляйте новый архитектурный слой.
Что ломается на практике
Слишком ранняя абстракция
Команда заранее создаёт универсальные интерфейсы для хранилищ и внешних сервисов, хотя у приложения пока один сценарий и одна реализация. Абстракции приходится синхронизировать с каждым новым требованием; они не дают свободы, а требуют дополнительного обслуживания. Вводите границу, когда есть понятная причина: тестирование важного правила, необходимость нескольких реализаций или нежелательная зависимость от инфраструктуры.
Слишком широкие модули
Общая папка «заказы» тоже может стать новой свалкой, если туда складывают всё, что хоть раз коснулось заказа. Название модуля не гарантирует связность. Проверьте, меняются ли лежащие рядом компоненты по одной и той же причине и можно ли описать их общую ответственность без перечня несвязанных функций.
Тесты проверяют только провода
Если тесты подтверждают лишь вызов одного класса из другого, они закрепляют текущую структуру, но не доказывают правильность правила. Для предметных ограничений проверяйте наблюдаемое поведение: при запрещённом переходе заказ не меняет состояние. Для интеграции с базой отдельно проверяйте то, что зависит от конкретного хранилища.
Где подход ломается: разделение на слои не решает автоматически распределённые транзакции, согласованность данных между сервисами и владение общими сущностями. Если несколько самостоятельных сервисов меняют одни и те же данные, это уже отдельный архитектурный вопрос — здесь мы рассматриваем границы внутри приложения.
Термин: прикладной слой
Прикладной слой1 связывает входящий запрос с нужным сценарием: вызывает правила, организует работу с зависимостями и возвращает результат вызывающей стороне. Он не обязан содержать всю бизнес-логику и не равен контроллеру: контроллер обычно относится к внешнему слою и переводит протокол запроса в вызов приложения.
1 В разных архитектурных подходах названия слоёв и их точные границы отличаются. Важнее не термин, а возможность показать, кто отвечает за правило и куда направлены зависимости.
Что делать дальше
Возьмите одну часто меняющуюся функцию и нарисуйте её путь от внешнего запроса до базы. Отметьте, где принимаются решения, где преобразуются форматы и кто управляет транзакцией. Затем выберите одну ошибку из списка и исправьте только её: например, вынесите предметное правило из контроллера, не перестраивая весь проект. После изменения сравните число затронутых компонентов и проверьте поведение тестами. Именно цена следующей правки покажет, помогли ли слои, а не аккуратность дерева каталогов.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Martin Fowler — Presentation Domain Data Layering, Microsoft — Common web application architectures
Частые вопросы
Обязательно ли делать отдельные слои для каждого проекта?
Должен ли контроллер содержать бизнес-логику?
Нужен ли интерфейс для каждого репозитория?
Как понять, что слоёв стало слишком много?
Нужно ли выбирать разные слои в зависимости от версии фреймворка?
Что делать, если один сценарий уже размазан по всему проекту?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.