Clean Architecture (Robert C. Martin, 2012). Concentric circles — Entities, Use Cases, Interface Adapters, Frameworks & Drivers — held together by the Dependency Rule (outer points inward). Four scenarios: happy-path request from controller through presenter to use case to entity to driven port to adapter; unit-testing the use case in isolation with in-memory repos; swapping a framework (Postgres -> Mongo) without touching the inner rings; and an antipattern showing what happens when the dependency rule is violated.
Clean Architecture (Robert C. Martin, 2012) — набор правил организации кода в концентрические слои с жёстким Dependency Rule: исходный код во внутреннем круге никогда не упоминает имена из внешнего. Бизнес-логика (Entities + Use Cases) не знает ничего про Postgres, Express, React — они «детали», которые подключаются снаружи через интерфейсы.
Четыре слоя:
Зависимости в коде идут только внутрь. Если внутреннему слою нужен внешний — внутренний объявляет интерфейс (port), а внешний его реализует (adapter). Стрелка в коде идёт от outer к inner; стрелка вызова в runtime — от inner к outer. Это Dependency Inversion.
Практический критерий: «можно ли поднять use case в unit-тесте без Postgres, без HTTP, без Stripe?» Если да — Dependency Rule соблюдён. Если нет — слои протекают.
Вложенные группы рисуют четыре кольца — снаружи внутрь: Frameworks & Drivers (Express, PostgreSQL, Stripe SDK) обёртывают Interface Adapters (HTTP Controller, PostgresOrderRepository, StripePaymentGateway, OrderPresenter), внутри них — Application Use Cases (CreateOrder UC, PayOrder UC + порты OrderRepository, PaymentGateway), и в самом центре — Entities (Order, Money VO, Customer).
Edges повторяют структуру кода, а не направление вызова в runtime:
controller-http -> usecase-create — controller импортирует use case (внешний знает про внутренний — OK).usecase-create -> port-repo — use case вызывает порт, объявленный рядом с ним (внутри своего кольца).repo-impl -> port-repo — implements. Стрелка идёт от outer adapter к inner port — это и есть Dependency Inversion на диаграмме.repo-impl -> postgres — единственное место в системе, где живёт SQL.В runtime пакет данных по edge repo-impl -> port-repo бежит в обратную сторону — это нормально (FlowBuilder автоматически анимирует reverse). Главное, что стрелка зависимости (кто кого знает в исходном коде) указывает строго внутрь.
Happy path POST /orders: запрос приходит из Express в HTTP Controller, тот зовёт CreateOrderUseCase через input boundary, use case применяет доменные правила на Order/Money, сохраняет через OrderRepository port — runtime-провод ведёт в PostgresOrderRepository, тот в Postgres. Результат поднимается обратно: use case вызывает Presenter, Presenter формирует ViewModel, controller возвращает 200 OK. Каждое кольцо знает только о следующем внутрь.
Unit-test use case без I/O: тест инжектит InMemoryOrderRepository вместо PostgresOrderRepository. Use case вызывает тот же OrderRepository port — меняется проводка, не код. Доменная логика на Order/Money работает на чистом языке (без фреймворков), in-memory adapter отвечает мгновенно, тест укладывается в ~5ms и не требует cleanup БД. Это и есть главный дивиденд Clean Architecture.
Замена PostgresOrderRepository на MongoOrderRepository: интерфейс OrderRepository port не меняется, поэтому use case, entities и controller не трогаем вообще. Меняется только outermost ring — провод от port к новой реализации, новый adapter, новый driver. Старая Postgres-ветка отмирает. Размер изменения локален «по конструкции», а не «по аккуратности».
Антипаттерн: entity импортирует Postgres driver напрямую (например, чтобы дёрнуть raw SQL ради «производительности»). Теперь Order нельзя протестировать без поднятого Postgres, и фреймворк диктует дизайн entity. Фикс — вернуть SQL в PostgresOrderRepository, а entity оставить чистой. Dependency Rule снова соблюдается, стрелки опять смотрят только внутрь.
Status: Accepted. Все три архитектуры — изоморфные ответы на одну проблему («не дать фреймворку диктовать дизайн»). Выбор реально влияет на ~5%, не на 50%.
| Критерий | Hexagonal (2005) | Onion (2008) | Clean (2012) |
|---|---|---|---|
| Метафора | Шестиугольник + ports | Луковица (rings) | 4 концентрических кольца |
| Главный термин | Port / Adapter | Domain Services | Use Case Interactor |
| Слоёв | 2 (core / adapter) | 3 (Domain / App / Infra) | 4 (Entity / UC / IA / FW) |
| Output port + Presenter | Опционально | Опционально | Явный |
| Prescriptiveness | Минимум | Средне | Максимум |
| Ceremony | Низкий | Средний | Высокий |
| Естественно ложится на | DDD-сервис, любой язык | .NET / Spring | Java/.NET enterprise, NestJS |
Decision:
Consequences: все три можно микшировать (Hexagonal core + Clean-style use case ports снаружи). Главное общее правило — Dependency Rule. Команда, не понимающая Dependency Inversion, одинаково сломает любой из трёх стилей.
За:
Против:
OrderRepository.findAll() возвращает QueryBuilder или ResultSet — use case теперь знает про БД.@Entity, @Column JPA-аннотации прямо в core entities. Теперь Entity знает про Hibernate.Не путайте Clean Architecture с запретом на use case orchestration: иногда правильно вызвать другой use case или общий доменный сервис; запрещено только тащить фреймворк внутрь.
Книги и статьи: