Screaming Architecture
Структура папок должна кричать о бизнесе, а не о фреймворке. Package-by-feature vs package-by-layer. Vertical Slice Architecture (Jimmy Bogard).
TL;DR
- Первое, что видит новый разработчик — структура папок. Она должна говорить о бизнесе, а не о фреймворке.
- Package-by-feature (orders/, payments/) обычно лучше package-by-layer (controllers/, services/, repositories/).
- Vertical Slice Architecture (Bogard) — крайняя форма: полный стек внутри одной feature-папки.
- Комбинирование layered внутри feature — разумный компромисс для средних проектов.
Тест: что кричит ваш проект
Your architectures should scream about the use cases of the application, and not about the frameworks you used.
Откройте свой проект. Что видит новый разработчик в корне?
Вариант A:
app/
├── controllers/
├── models/
├── serializers/
├── services/
├── views/
└── urls/
Проект кричит: «я Django/Rails-приложение». Что оно делает? Непонятно — нужно открывать файлы.
Вариант B:
app/
├── orders/
├── payments/
├── inventory/
├── shipping/
└── customers/
Проект кричит: «я система обработки заказов». Framework вторичен.
Uncle Bob (Martin) считает, что вторая структура — правильная. И даже жёстче: если по структуре папок нельзя понять, что делает система — это архитектурный смелл.
Package-by-layer vs package-by-feature
Структура, где верхнеуровневые пакеты соответствуют техническим слоям (controllers, services, repositories).
Структура, где верхнеуровневые пакеты соответствуют бизнес-возможностям (orders, payments, shipping).
Разница на реальном примере:
Package-by-layer:
app/
├── controllers/
│ ├── order_controller.py
│ ├── payment_controller.py
│ └── shipping_controller.py
├── services/
│ ├── order_service.py
│ ├── payment_service.py
│ └── shipping_service.py
├── repositories/
│ ├── order_repository.py
│ ├── payment_repository.py
│ └── shipping_repository.py
└── models/
├── order.py
├── payment.py
└── shipping.py
Package-by-feature:
app/
├── orders/
│ ├── controller.py
│ ├── use_cases.py
│ ├── repository.py
│ └── model.py
├── payments/
│ ├── controller.py
│ ├── use_cases.py
│ ├── repository.py
│ └── model.py
└── shipping/
├── controller.py
├── use_cases.py
├── repository.py
└── model.py
Почему feature-first обычно лучше
Изменения локализованы. Новая фича по заказам трогает одну папку orders/, а не пять разных папок.
Соблюдение принципа Common Closure. «То, что меняется вместе, лежит вместе.» Классика (Martin).
Легче удалять. Ушёл функционал shipping/ — удалили папку. В package-by-layer — придётся выковыривать из 5 разных папок.
Явные границы модулей. Каждая feature-папка — потенциально bounded context. Легче потом резать на микросервисы.
Meaningful diff в PR. Изменения по одной feature = список файлов в одной папке. Ревьюеру ясно.
Когда package-by-layer оправдан
Не всегда feature-first лучше:
- Очень маленький проект — 3-5 файлов. Дробить на feature-папки = искусственное усложнение.
- Строгое разделение команд по слоям — фронтендеры, бэкендеры, DBA. Редко и уже проблема сама по себе.
- Много кросс-фичевого кода — если каждая фича трогает одни и те же таблицы, границы фич не проходят.
Vertical Slice Architecture
Jimmy Bogard (автор MediatR) довёл идею до логического конца в 2018-м:
Rather than have a set of technical layers, we organize code around business features. Each slice contains everything needed to fulfill that feature.
Организация кода по сценариям (use case). Внутри слайса — полный стек: контроллер, use case, model, доступ к данным. Между слайсами — минимум общего кода.
Пример:
app/
├── orders/
│ ├── place_order/
│ │ ├── endpoint.py # POST /orders
│ │ ├── command.py # PlaceOrderCommand
│ │ ├── handler.py # PlaceOrderHandler
│ │ ├── validator.py
│ │ └── response.py
│ ├── cancel_order/
│ │ ├── endpoint.py
│ │ ├── command.py
│ │ ├── handler.py
│ │ └── response.py
│ └── shared/
│ ├── models.py # Order aggregate
│ └── repository.py
└── shipping/
├── ship_order/
│ └── ...
└── track_shipment/
└── ...
Каждый use case — своя папка. Никакого «Order Service» с 15 методами. Одна папка = один сценарий.
Bogard не претендует на универсальность. Но для CQRS-стеков и MediatR-подобных решений — Vertical Slice оптимален.
Комбинация: слои внутри features
Практический компромисс:
app/
├── orders/ # Bounded context
│ ├── domain/ # entities, VO, aggregates
│ ├── application/ # use cases + ports
│ ├── infrastructure/ # repositories, adapters
│ └── interface/ # HTTP handlers
├── payments/
│ ├── domain/
│ ├── application/
│ ├── infrastructure/
│ └── interface/
└── shared/
├── kernel/ # общие Value Objects (Money, DateRange)
└── platform/ # логгер, метрики, DI
Верхний уровень — feature-first (screaming). Внутри — layer-first (Hexagonal/Clean).
Плюсы:
- Границы модулей (bounded contexts) явные.
- Внутри модуля — привычная Hexagonal-структура.
shared/минимален — только общие примитивы.
Этот подход — де-факто стандарт для модульных монолитов.
Как не надо
1. «Utils» на верхнем уровне
app/
├── orders/
├── payments/
├── utils/ # !!
│ ├── helpers.py
│ ├── decorators.py
│ └── constants.py
utils/ — свалка. Со временем туда попадает всё, что «не подошло» в feature-папку. К году там 200 файлов, из которых ни одна feature не может отказаться, потому что все от них зависят.
Правильно: конкретный helper живёт рядом с использующим кодом. Общие примитивы (Money, DateRange) — в shared/kernel/.
2. Тонкие feature-папки
app/
└── orders/
├── controller.py # 5 строк
├── service.py # 500 строк (весь код)
└── repository.py # 20 строк
Feature-first, но внутри — та же процедурная layered. Ничего не выиграли.
Проблема: orders/service.py — жирный сервис. См. главу Layered — там разбирали.
3. Feature-папка ≠ bounded context
app/
├── users/
│ ├── auth.py
│ ├── profile.py
│ ├── notifications.py
│ └── billing.py
users/ — это не feature. Это domain «пользователи», в котором сидят auth, profile, notifications, billing — четыре разных контекста. Всё, что связано со словом «user», сброшено в одну папку.
Правильно: разделить на auth/, profile/, notifications/, billing/. Каждый — bounded context. Внутри — своя модель User (Auth-User ≠ Billing-User).
4. Framework в имени папки
app/
├── flask_routes/
├── celery_tasks/
├── sqlalchemy_models/
Проект кричит не «я система заказов», а «я использую Flask + Celery + SQLAlchemy». Это анти-Screaming Architecture. Смена фреймворка = ломание структуры.
Правильно: имена по бизнес-функциям, framework — infrastructure detail.
Тесты Screaming Architecture
Три быстрых теста для существующего проекта:
Тест закрытой книги. Закройте README.md, откройте структуру папок. Понятно ли, чем система занимается? Если нужны файлы — Screaming не соблюдён.
Тест удаления фичи. Удалите одну бизнес-фичу. Сколько папок и файлов затронуто? Больше 3 в разных пакетах — фичи размазаны.
Тест grep по фреймворку. grep -r "flask\|django\|fastapi" app/domain/. Ноль совпадений — хорошо. Ненулевые — фреймворк проник в бизнес-слой.
Trade-offs
| Ситуация | Package-by-feature | Package-by-layer |
|---|---|---|
| Проект > 20 файлов | ✓ | |
| Планируется дробить на микросервисы | ✓ | |
| Команда > 3 человек | ✓ | |
| Многие изменения затрагивают одну feature | ✓ | |
| Проект — 5 файлов «hello world» | ✓ (не критично) | |
| Строгое разделение по слоям в команде | ✓ (редко оправдано) |
Правило большого пальца: если проект живёт > 3 месяцев или команда > 2 человек — feature-first.
В твоём же коде
Анонимизированный микросервис использует package-by-layer:
app/
├── application/
├── domain/
├── infrastructure/
├── core/
└── interface/
Для сервиса с одним use case это работает — «feature» и «сервис» совпадают. Проект кричит: «я сервис-оркестратор с чётким Hexagonal».
Если бы сервис разросся до 5 use case’ов (нормализация + reprocess + retry + audit + export), лучше стало бы:
app/
├── standardize/ # feature
│ ├── domain/
│ ├── application/
│ └── infrastructure/
├── reprocess/
├── audit/
├── export/
└── shared/
└── kernel/ # общие Value Objects
Границы фич стали бы явными, и каждую можно было бы обсуждать отдельно.
Дальнейшее чтение
- Robert C. Martin. Screaming Architecture. 2011. Оригинальный пост.
- Robert C. Martin. Clean Architecture. Chapter 21 — Screaming Architecture целиком.
- Jimmy Bogard. Vertical Slice Architecture. 2018. Feature-first доведён до предела.
- Simon Brown. Modular Monoliths. Как feature-first ведёт к модульному монолиту.
- Herberto Graça. Screaming Architecture. Хороший разбор с картинками.
Проверьте себя
Мини-quiz · закрепить
Проверьте себя
Q1. Что должна «кричать» структура папок правильно спроектированного приложения?
Q2. Когда package-by-layer предпочтительнее package-by-feature?
Q3. Vertical Slice Architecture отличается от простого package-by-feature тем, что...
Q4. Какие проблемы у Vertical Slice Architecture?
Q5. Тест «закрытой книги» (для проверки Screaming Architecture) — это...