Clean Architecture и DDD 2026: паттерны для backend-разработчика

Гайд по Clean Architecture и Domain-Driven Design 2026 — слои, агрегаты, события, паттерны и реальные ошибки внедрения.

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 хорошо сочетается с обоими подходами, но не требует их по умолчанию.

  1. Начните с обычных use cases и доменной модели.
  2. Выделите read models только для тяжёлых экранов и отчётов.
  3. Добавьте outbox для надёжной публикации событий.
  4. Переходите к 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, опишите доменные объекты, спрячьте внешние зависимости за интерфейсами, добавьте тесты. Старый код пусть вызывает новый сценарий через адаптер. Так система меняется без остановки поставки фич.

  1. Найдите 3-5 сценариев, которые чаще всего ломаются или меняются.
  2. Опишите текущие правила словами бизнеса.
  3. Выделите доменные типы и инварианты.
  4. Создайте use case вокруг одного сценария.
  5. Покройте его unit- и integration-тестами.
  6. Подключите старый интерфейс к новому коду.

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 — мы держим эту страницу актуальной.

Поделиться: Telegram X LinkedIn