Clean Architecture в 2026 — это не модная диаграмма с кругами, а способ удержать backend-систему управляемой, когда команда растёт, фреймворки меняются, а бизнес каждую неделю приносит новые правила. В этом гайде разберём, как совместить чистую архитектуру и Domain-Driven Design: слои, агрегаты, события, CQRS, тесты, миграцию и ошибки, которые дорого чинить после релиза.
Цены, лимиты, версии продуктов и зарплатные диапазоны в материале даны как ориентиры на момент публикации. Точные значения сверяйте по сайтам провайдеров и актуальным исследованиям рынка.
Зачем Clean Architecture в 2026
Backend стал сложнее, а терпимость к хаосу ниже
Clean Architecture нужна там, где код живёт дольше одного релизного цикла. В 2026 backend редко ограничивается REST API и одной PostgreSQL. Типичный продукт тянет за собой очереди, внешние API, ML-сервисы, биллинг, feature flags, observability, мобильные клиенты, админки и несколько команд, которые параллельно меняют одну бизнес-область.
Проблема не в том, что фреймворк плохой. Проблема в том, что бизнес-правила часто оказываются размазаны по контроллерам, ORM-моделям, cron-задачам, подписчикам очередей и SQL-миграциям. Через 12-18 месяцев команда уже не уверена, где именно считается скидка, кто может отменить заказ и почему возврат денег иногда проходит дважды.
Clean Architecture предлагает простую сделку: доменная логика не зависит от транспорта, базы данных, брокера сообщений и веб-фреймворка. Это не гарантирует красивый код автоматически, но даёт направление для решений: что можно менять без боли, а что должно оставаться устойчивым.
Где подход окупается
Чистая архитектура особенно полезна в системах, где много правил и долгий жизненный цикл: финтех, e-commerce, логистика, HR-tech, EdTech, B2B SaaS, страхование, медицинские платформы. Если продукт живёт 3-7 лет, а не 3 месяца, цена архитектурной беспечности растёт быстрее, чем кажется на старте.
| Ситуация | Без архитектурных границ | С явными слоями |
|---|---|---|
| Замена REST на gRPC | Переписываются контроллеры и часть бизнес-логики | Меняется адаптер транспорта |
| Новая платёжная система | Условия оплаты расползаются по сервисам | Добавляется gateway и use case |
| Рост команды с 5 до 25 разработчиков | Конфликты в одних и тех же файлах | Границы модулей видны в коде |
Когда не стоит усложнять
Если вы пишете одноразовый скрипт, простой CRUD для внутренней формы или MVP на 2-4 недели, полная схема может быть избыточной. Но даже там полезно не складывать бизнес-правила в HTTP-контроллеры. Минимальная дисциплина стоит дёшево: отдельные use cases, интерфейсы для внешних систем, доменные типы вместо голых строк и чисел.
Главный критерий: сколько стоит изменение правила. Если добавление нового статуса заказа занимает 3 часа и не ломает соседние сценарии, всё нормально. Если на это уходит 3 дня, 8 файлов и ручное тестирование половины продукта, архитектура уже выставила счёт.
Слои: Entities, Use Cases, Adapters, Frameworks
Entities: ядро бизнес-правил
Entities — это не обязательно ORM-сущности. В чистой архитектуре entity выражает бизнес-понятие и его инварианты: заказ не может быть оплачен отрицательной суммой, подписка не может активироваться без тарифа, заявка на отпуск не может пересекать заблокированный период. Эти правила должны работать одинаково из API, фоновой задачи и консольной команды.
В хорошей модели entity не знает, что её сохраняют в PostgreSQL, MongoDB или Redis. Она не вызывает HTTP-клиент и не читает переменные окружения. Её задача — защищать смысл. Например, объект Money хранит сумму и валюту, а не просто decimal; Email проверяет формат; Period не допускает дату окончания раньше даты начала.
Use Cases: сценарии приложения
Use case отвечает на вопрос: что пользователь или система хочет сделать? Зарегистрировать кандидата, подтвердить платёж, назначить курьера, пересчитать бонусы, закрыть отчётный период. В Clean Architecture use case координирует сущности, репозитории, внешние шлюзы и транзакции, но не превращается в свалку условий.
Пример нормального use case: ConfirmPayment. Он загружает заказ, проверяет статус, применяет оплату, сохраняет изменения, публикует доменное событие. Пример плохого use case: OrderService на 2500 строк, где есть создание, отмена, доставка, промокоды, возвраты и экспорт в Excel.
- Entity хранит правила, которые верны всегда.
- Use case описывает конкретный сценарий приложения.
- Repository interface задаёт контракт хранения.
- Gateway прячет внешние API.
- Presenter или DTO готовит данные для клиента.
Adapters и Frameworks: внешнее кольцо
Adapters переводят внешний мир на язык приложения. HTTP-контроллер превращает JSON в command-объект. ORM-репозиторий превращает строки таблицы в доменную модель. Kafka-consumer принимает сообщение и вызывает use case. Frameworks — это самый внешний слой: Django, Spring Boot, NestJS, Laravel, FastAPI, Express, .NET, Rails.
Практическое правило простое: чем ближе код к бизнесу, тем меньше он должен знать о технологических деталях. Контроллер может знать про HTTP 400, use case — нет. Репозиторий может знать SQL, entity — нет. Это делает систему скучнее в хорошем смысле: меньше сюрпризов, меньше магии, меньше причин бояться обновления библиотеки.
Dependency Rule и инверсия зависимостей
Зависимости направлены внутрь
Dependency Rule говорит: внутренние слои не зависят от внешних. Домен не импортирует ORM. Use case не импортирует веб-контроллер. Бизнес-правило не знает, что его вызвали из REST, GraphQL, CLI или очереди. В Clean Architecture это не эстетика, а защита от распространённой болезни backend-кода: фреймворк начинает диктовать модель бизнеса.
На практике правило проверяется очень грубо: откройте доменный пакет и посмотрите импорты. Если там есть express, django.db, sqlalchemy, javax.persistence, prisma, nestjs decorators или HTTP status codes, граница уже протекает. Иногда это удобно первые 2 месяца, но через год миграция или тестирование становятся дороже.
Инверсия через интерфейсы
Инверсия зависимостей не означает «наплодить интерфейсы на всё». Она нужна в точках, где внутренний слой должен использовать внешний механизм: базу, очередь, платёжный провайдер, почтовый сервис, файловое хранилище. Use case зависит от абстракции PaymentGateway, а Stripe, YooKassa или CloudPayments становятся реализациями.
| Неправильно | Лучше | Почему |
|---|---|---|
| Use case вызывает Stripe SDK | Use case вызывает PaymentGateway | Можно заменить провайдера и тестировать без сети |
| Entity наследует ORM BaseModel | Entity отделена от persistence model | Домен не зависит от схемы таблиц |
| Контроллер содержит расчёт скидки | Контроллер вызывает ApplyDiscountUseCase | Правило доступно из API, очереди и тестов |
Где провести границу
Граница должна проходить там, где изменение внешней технологии не должно менять бизнес-логику. Но не надо доводить идею до карикатуры. Абстракция над стандартной библиотекой дат обычно бесполезна. Абстракция над платёжным провайдером — почти всегда полезна. Абстракция над ORM нужна, если доменная модель сложнее обычного CRUD.
Команды часто ошибаются в обе стороны. Одни пишут «чистый» код с 40 интерфейсами на 10 классов и получают архитектурный театр. Другие до последнего держат всю систему в контроллерах, потому что «так быстрее». Рабочий компромисс: вводить интерфейсы на границах с нестабильными и дорогими зависимостями. База данных, брокер, внешнее API, платёжка, почта, SMS, object storage — хорошие кандидаты.
DDD: ubiquitous language, контексты
Ubiquitous language: один язык для бизнеса и кода
Domain-Driven Design начинается не с агрегатов, а с языка. Ubiquitous language — это общий словарь, которым пользуются разработчики, аналитики, продакты, саппорт и бизнес. Если в коде «user», в CRM «клиент», в бухгалтерии «контрагент», а в отчётах «аккаунт», команда уже платит налог на перевод.
В DDD название класса — не декор. Оно должно совпадать с реальным понятием домена. В логистике «shipment», «route», «delivery attempt» и «pickup window» не одно и то же. В HR-tech «candidate», «applicant», «employee» и «talent pool member» тоже разные сущности. Если эти различия потерять, код начнёт принимать неверные состояния.
- Собирайте термины на воркшопах с доменными экспертами.
- Фиксируйте спорные понятия в glossary, а не в Slack-тредах.
- Переименовывайте код, если бизнес уточнил термин.
- Не используйте технические названия там, где есть доменное слово.
- Проверяйте язык в тестах: сценарии должны читаться как бизнес-описание.
Bounded Context: границы смысла
Bounded Context — это область, где термин имеет одно значение. «Order» в интернет-магазине, складской системе и бухгалтерии может быть тремя разными моделями. Для покупателя заказ — корзина, доставка и оплата. Для склада — набор позиций к сборке. Для бухгалтерии — основание для закрывающих документов.
Попытка сделать одну универсальную модель Order на всю компанию обычно заканчивается объектом с 60 полями, половина из которых nullable. В 2026 это особенно заметно в компаниях с микросервисами: физическое разделение сервисов есть, а смысловые границы не проведены. Получается распределённый монолит, только дороже в эксплуатации.
Context Map
Context Map показывает, как контексты связаны между собой. Например, Billing зависит от Sales, Warehouse публикует события для Delivery, HR Core отдаёт данные в Payroll. Это не UML ради UML, а карта договорённостей: кто владелец данных, где источник истины, какие события публикуются, где нужна антикоррупционная прослойка.
| Связь контекстов | Пример | Риск |
|---|---|---|
| Customer/Supplier | Billing использует данные Sales | Поставщик меняет контракт без предупреждения |
| Shared Kernel | Общие типы Money и Currency | Общая библиотека становится свалкой |
| Anti-Corruption Layer | Интеграция с legacy ERP | Чужая модель просачивается в домен |
Агрегаты, value objects, доменные события
Агрегат защищает инварианты
Агрегат — это группа объектов, которая меняется как единое целое. У него есть root, через который проходят все изменения. В заказе root может контролировать позиции, оплату и статус. В банковском счёте — баланс и операции. В расписании — слот, бронирование и ограничения.
Главный вопрос при проектировании агрегата: какие инварианты должны быть атомарными? Если заказ нельзя подтвердить без хотя бы одной позиции и валидной доставки, это правило должно жить внутри агрегата или рядом с ним, а не в контроллере. Если инварианты не связаны транзакционно, возможно, вы пытаетесь сделать агрегат слишком большим.
Размер агрегата важен. Маленький агрегат проще блокировать, сохранять и тестировать. Большой агрегат может стать узким местом: лишние блокировки, тяжёлые загрузки, конфликты при параллельных изменениях. В системах с высокой нагрузкой часто лучше иметь 3-5 небольших агрегатов и согласовывать их событиями, чем один «бог-объект».
Value Objects вместо примитивов
Value Object описывает значение без собственной идентичности. Деньги, email, телефон, период дат, координаты, процент скидки, налоговая ставка — хорошие кандидаты. Они уменьшают количество неявных правил. Вместо пары amount и currency появляется Money, который не позволит сложить рубли с евро без явной конвертации.
- Money: сумма, валюта, правила округления.
- Email: нормализация регистра и базовая валидация.
- DateRange: начало не позже окончания.
- Percentage: диапазон от 0 до 100 или от 0 до 1.
- Address: страна, город, индекс, строка доставки.
Доменные события
Доменное событие фиксирует факт, который уже произошёл: OrderPaid, CandidateHired, SubscriptionExpired, InvoiceIssued. Оно помогает развязать части системы. После оплаты заказа не нужно внутри одного метода отправлять письмо, начислять бонусы, обновлять аналитику и запускать доставку. Агрегат публикует событие, подписчики делают своё.
Важно не путать доменные события с техническими. «RowUpdated» или «MessageReceived» ничего не говорят бизнесу. Хорошее событие понятно человеку из предметной области. В Clean Architecture событие рождается внутри домена или use case, а транспортом наружу занимается адаптер: Kafka, RabbitMQ, NATS, outbox-таблица или внутренний event bus.
CQRS и Event Sourcing — когда применять
CQRS: разделяем команды и чтение
CQRS разделяет операции изменения состояния и операции чтения. Command отвечает за действие: создать заказ, подтвердить email, списать бонусы. Query отвечает за получение данных: список заказов, карточка клиента, отчёт по продажам. Это полезно, когда модель записи и модель чтения конфликтуют.
Например, для оформления заказа нужна строгая доменная модель с инвариантами. Для личного кабинета нужна быстрая витрина: статус, дата, сумма, доставка, последние события. Заставлять одну модель одинаково хорошо обслуживать оба сценария — частая причина сложных ORM-запросов и хрупкого кода.
| Признак | CQRS полезен | CQRS избыточен |
|---|---|---|
| Нагрузка | Чтений в 5-20 раз больше, чем записей | CRUD с десятками операций в день |
| Модель | Сложные правила записи, разные витрины чтения | Форма почти совпадает с таблицей |
| Команда | Есть опыт асинхронности и eventual consistency | Команда впервые делит монолит |
Event Sourcing: состояние как история событий
Event Sourcing хранит не только текущее состояние, а последовательность событий: AccountOpened, MoneyDeposited, MoneyWithdrawn, LimitChanged. Текущее состояние восстанавливается проигрыванием событий. Это мощно в доменах, где важна история: финансы, аудит, логистика, страхование, trading, биллинг.
Но Event Sourcing дорог. Нужно проектировать версии событий, snapshots, replay, идемпотентность, миграции, read models, обработку дублей и задержек. Если команда хочет Event Sourcing только потому, что «так делают взрослые», стоит притормозить. Для многих продуктов достаточно обычной записи состояния плюс audit log и outbox.
Практический критерий выбора
Применяйте CQRS, когда чтение реально отличается от записи. Применяйте Event Sourcing, когда история событий является бизнес-ценностью, а не побочным логом. В остальных случаях держите архитектуру проще. Clean Architecture хорошо сочетается с обоими подходами, но не требует их по умолчанию.
- Начните с обычных use cases и доменной модели.
- Выделите read models только для тяжёлых экранов и отчётов.
- Добавьте outbox для надёжной публикации событий.
- Переходите к Event Sourcing после проверки домена и команды.
Тестирование: unit, integration, contract
Unit-тесты для домена и use cases
Тестирование — одна из главных причин внедрять Clean Architecture. Когда домен не зависит от базы и HTTP, его можно тестировать быстро: сотни unit-тестов за секунды. Проверяются не mocks ради mocks, а бизнес-правила: нельзя списать больше баланса, нельзя отправить заказ без адреса, нельзя активировать истёкший промокод.
Хороший unit-тест читает сценарий, а не устройство фреймворка. Он создаёт доменные объекты, вызывает метод или use case, проверяет результат и события. Если для проверки скидки нужно поднять контейнер с PostgreSQL, Redis и Kafka, значит правило спрятано слишком далеко от домена.
Integration-тесты для адаптеров
Integration-тесты нужны там, где ваша абстракция встречается с реальностью: SQL-запросы, транзакции, миграции, брокеры, внешние API, сериализация. Репозиторий может иметь идеальный интерфейс и всё равно падать на nullable-колонке или неверном индексе. Поэтому адаптеры надо проверять с настоящими зависимостями или максимально близкими контейнерами.
| Тип теста | Что проверяет | Ориентир по времени |
|---|---|---|
| Unit | Домен, value objects, use cases | 1-100 мс на тест |
| Integration | База, брокер, файловое хранилище | 100 мс - 5 с на тест |
| Contract | API между сервисами | Секунды или минуты на набор |
| E2E | Критический пользовательский путь | Минуты на прогон |
Contract-тесты для микросервисов
В распределённых системах самый неприятный баг часто живёт не внутри сервиса, а между сервисами. Producer поменял поле, consumer не готов, staging зелёный, production красный. Contract-тесты фиксируют ожидания сторон: формат события, обязательные поля, версии API, допустимые значения.
Для событий полезны schema registry, JSON Schema, Protobuf или Avro. Для HTTP — OpenAPI-контракты и consumer-driven contracts. Не надо покрывать контрактами всё подряд. Начните с платежей, заказов, статусов пользователей, прав доступа и других потоков, где ошибка стоит денег или блокирует работу.
Реальные кейсы внедрения
E-commerce: заказ как доменный центр
В интернет-магазине на 200-500 тыс. заказов в месяц типичная боль — заказ оброс логикой оплаты, доставки, промокодов, возвратов и склада. До рефакторинга правила часто живут в контроллерах и SQL-процедурах. Любое изменение промоакции требует проверки оплаты, доставки и отчётов.
Практичный вариант внедрения: выделить bounded contexts Sales, Payment, Fulfillment и Catalog. В Sales остаются корзина, заказ, скидки и статусы покупателя. Payment отвечает за авторизацию, списание и возвраты. Fulfillment занимается сборкой и доставкой. Между ними идут события OrderPlaced, PaymentCaptured, ShipmentCreated, RefundApproved.
Результат обычно не мгновенный. Первые 2-3 месяца команда тратит больше времени на границы и тесты. Зато через 6-9 месяцев изменения в промо и доставке перестают ломать оплату. Для бизнеса это означает меньше регрессий в пиковые периоды: распродажи, ноябрь-декабрь, запуск нового региона.
Fintech: инварианты дороже скорости
В финтехе архитектурная небрежность быстро превращается в финансовый риск. Нельзя «примерно» обработать списание, дважды провести возврат или потерять событие смены лимита. Здесь Clean Architecture помогает отделить правила продукта от интеграций с банками, процессингом, KYC-провайдерами и антифродом.
Частый паттерн: Account, LedgerEntry, Limit, CustomerRiskProfile как доменные понятия; outbox для событий; идемпотентные команды; audit log для операций. Event Sourcing может быть оправдан, если история операций нужна для аудита и расследований. Но даже без него полезно хранить неизменяемые записи проводок, а не только текущий баланс.
B2B SaaS: роли, тарифы и биллинг
В SaaS-системах сложность редко видна в первый квартал. Потом появляются тарифы, лимиты, trial, grace period, enterprise-договоры, ручные скидки, SSO, роли, команды, несколько рабочих пространств. Если всё это лежит в UserService, продукт начинает буксовать.
Рабочая декомпозиция: Identity управляет пользователями и входом, Billing — подписками и платежами, Entitlements — правами на функции, Workspace — командами и ресурсами. Важно не смешивать роль пользователя в команде и право пользоваться платной функцией. Это разные модели, хотя на экране они могут выглядеть как один переключатель.
Типичные ошибки и анти-паттерны
Анемичная модель
Самая частая ошибка — назвать проект Clean Architecture, но оставить всю логику в сервисах. Entities превращаются в структуры данных с getters и setters, use cases становятся procedural scripts, а домен ничего не защищает. Это называется анемичной моделью: снаружи выглядит прилично, внутри правил нет.
Признак простой: если объект Order можно перевести из New сразу в Delivered простым присваиванием статуса, модель не защищает инварианты. Лучше дать объекту метод confirmPayment, ship или cancel, внутри которого проверяются допустимые переходы и создаются события.
Абстракции без причины
Противоположная ошибка — построить архитектурный храм на пустом месте. Интерфейс для каждого класса, фабрика для каждой фабрики, маппер для маппера, отдельный модуль ради трёх строк. Код становится «чистым» только в презентации, а разработчики тратят время на навигацию между слоями.
- Не создавайте repository, если нет доменной модели и сложных запросов.
- Не вводите CQRS для пяти CRUD-экранов.
- Не делайте Event Sourcing без бизнес-требования к истории.
- Не прячьте простую функцию за четырьмя интерфейсами.
- Не называйте папки domain и application, если зависимости всё равно текут наружу.
Технические границы вместо доменных
Папки controllers, services, repositories и models не равны DDD. Это техническое разделение. Оно может быть нормальным на малом проекте, но в крупной системе полезнее группировать код вокруг бизнес-возможностей: orders, payments, subscriptions, candidates, invoices. Внутри каждого модуля уже могут быть свои слои.
Ещё один анти-паттерн — общий shared-модуль, куда постепенно попадает всё: utils, validators, dto, constants, errors, clients. Через год shared становится самым связанным местом в системе. Держите общее маленьким: Money, Currency, DateRange, базовые ошибки, tracing. Всё остальное пусть живёт в контекстах.
Как мигрировать с спагетти-кода на Clean
Не переписывайте всё сразу
Миграция на Clean Architecture редко должна начинаться с большого переписывания. Полный rewrite на 6-12 месяцев почти всегда проигрывает постепенной замене: бизнес продолжает просить функции, старый код продолжает жить, новая версия отстаёт, команда теряет доверие. Лучше двигаться вертикальными срезами.
Выберите один проблемный сценарий: отмена заказа, возврат платежа, смена тарифа, расчёт комиссии. Вынесите его в use case, опишите доменные объекты, спрячьте внешние зависимости за интерфейсами, добавьте тесты. Старый код пусть вызывает новый сценарий через адаптер. Так система меняется без остановки поставки фич.
- Найдите 3-5 сценариев, которые чаще всего ломаются или меняются.
- Опишите текущие правила словами бизнеса.
- Выделите доменные типы и инварианты.
- Создайте use case вокруг одного сценария.
- Покройте его unit- и integration-тестами.
- Подключите старый интерфейс к новому коду.
Strangler Fig Pattern
Strangler Fig Pattern — подход, при котором новая реализация постепенно обрастает старую и заменяет её частями. Для backend это обычно выглядит так: старый endpoint остаётся, но внутри начинает вызывать новый application layer. Потом часть логики переезжает в домен, затем адаптеры, затем старый сервис удаляется.
Важно вести реестр миграции. Не в голове тимлида, а в видимом списке: сценарий, владелец, текущий путь кода, новая реализация, тесты, дата удаления старого участка. Для команды из 8-15 backend-разработчиков такой список экономит часы обсуждений каждую неделю.
Метрики прогресса
Архитектурную миграцию надо измерять. Не только количеством новых папок, а практическими метриками: время изменения правила, количество регрессий, покрытие ключевых сценариев, число циклических зависимостей, доля бизнес-логики вне контроллеров. Если через 3 месяца стало больше файлов, но релизы не ускорились и багов меньше не стало, значит команда занимается косметикой.
| Метрика | Плохой сигнал | Здоровый ориентир |
|---|---|---|
| Изменение бизнес-правила | 2-5 дней и ручная проверка | 2-8 часов плюс автотесты |
| Unit-тесты домена | Почти отсутствуют | Покрыты критические инварианты |
| Циклические зависимости | Модули импортируют друг друга | Зависимости направлены к ядру |
Финальная цель не в том, чтобы «перейти на Clean» как на новый бренд кода. Цель — сделать систему, где изменения бизнес-логики локальны, тесты быстры, а разработчик может понять сценарий без экскурсии по 25 файлам.
Глубже на тему — исследования it-institute.ru
На партнёрском портале it-institute.ru опубликована подборка релевантных исследований с медианами, выборками и методологией:
FAQ о Clean Architecture
Clean Architecture подходит только для микросервисов?
Нет. Она часто лучше всего раскрывается в модульном монолите, где границы проще контролировать. Микросервисы без доменных границ быстро превращаются в распределённый монолит.
Можно ли использовать Clean Architecture с Django, Laravel, NestJS или Spring?
Да, но фреймворк должен оставаться во внешнем слое. Контроллеры, ORM-модели и decorators не должны определять доменную модель и правила use cases.
DDD обязателен для чистой архитектуры?
Нет, но они хорошо дополняют друг друга. Clean Architecture даёт направление зависимостей, а DDD помогает правильно выделить смысловые границы, агрегаты и язык домена.
Сколько слоёв нужно делать в реальном проекте?
Обычно хватает domain, application, adapters и infrastructure. В маленьком сервисе часть слоёв может быть тонкой, но направление зависимостей лучше сохранить.
Когда CQRS становится оправданным?
Когда модель чтения заметно отличается от модели записи или чтений в разы больше, чем изменений. Для обычного CRUD CQRS часто добавляет больше сложности, чем пользы.
Нужно ли отделять доменные сущности от ORM-моделей?
Если домен сложный — да, это снижает зависимость правил от схемы хранения. В простом CRUD можно начать с ORM, но не стоит класть расчёты и инварианты в контроллеры.
Как понять, что внедрение идёт правильно?
Изменения правил становятся локальнее, unit-тесты домена быстрее, а контроллеры тоньше. Если кода стало больше, но менять продукт всё так же страшно, архитектура пока не решила главную проблему.
Следите за обновлениями itech-news.ru — мы держим эту страницу актуальной.
