Хоть я и обещал себе больше не возвращаться к теме DDD, но все таки добавлю еще пару слов на эту тему. Если коротко, то не делайте из еды культа. Посмотрите по сторонам.
Далее: Пара слов про Domain-Driven Design (DDD)
Ключевой вопрос при выборе архитектуры: где источник сложности? Если сложность в технической части (большие объёмы, распределённость) — вам нужны другие инструменты. Если сложность в самих бизнес-правилах, их количестве и скорости изменений — вот тут и проявляется DDD.
Практический чек-лист из 10 вопросов
Отвечайте «Да» или «Нет». Если набрали 6+ «Да» — DDD принесёт пользу. Если меньше 4 — бегите от DDD, используйте простой CRUD + Transaction Script.
Бизнес и логика (главное)
-
Правила меняются чаще, чем таблицы в БД?
Новые скидки, статусы заказов, логика одобрения кредитов появляются каждые 2 недели. -
Одна и та же сущность (например, «Заказ») означает разное для разных отделов?
Для бухгалтерии — это проводки, для склада — коробки, для поддержки — статус жалобы. -
Бизнес-эксперты используют термины, которых нет в коде?
В кодеstate = 5, а в бизнесе говорят «В ожидании верификации». -
Для расчёта одной операции нужно собрать данные из 5+ таблиц с кучей JOIN’ов?
Команда и процесс
-
В команде есть сеньоры, готовые обучать DDD, и бюджет на обучение?
-
Бизнес-эксперты (не продакт-менеджеры!) готовы уделять 2+ часа в неделю на обсуждение моделей?
-
Проект рассчитан на жизнь от 3 лет (не стартап на выброс)?
Технические триггеры
-
У вас микросервисы, и вы ловите себя на том, что дублируете одну логику в 3 сервисах?
Признак, что не выделены Bounded Contexts. -
В коде полно
if (type == "A") { ... } else if (type == "B")— и их уже больше 20 штук? -
Вы часто правите старый код и боитесь что-то сломать, потому что логика «размазана» по контроллерам, сервисам и хелперам?
Результат
- 8–10 Да → ✅ Берите DDD в полный рост (с Event Sourcing, если нужен аудит).
- 6–7 Да → 🟡 Возьмите только тактику (Value Objects, Aggregates), без CQRS и сложной архитектуры.
- 4–5 Да → 🟠 Используйте Domain Events как лёгкий паттерн, остальное — классический слоистый MVC.
- 0–3 Да → 🔴 Категорически нет. Используйте ActiveRecord или Table Module — это спасёт ваши нервы.
Бонус: «Тест на 5 секунд»
Откройте любой класс сущности. Если там есть геттеры/сеттеры для всех полей — вы уже нарушаете инкапсуляцию. DDD требует, чтобы изменение статуса шло через метод order.approve(), а не через order.set_status(5). Если это кажется вам неестественным — DDD не для вас.
Альтернативные архитектурные паттерны
Если чек-лист показал «Нет» или «не совсем» — это не приговор, а повод присмотреться к другим паттернам. Ниже — альтернативы, на которые стоит обратить внимание, от простых к сложным.
1. Transaction Script
Организация логики как последовательности шагов в одном сервисе или контроллере: получил данные → проверил → изменил → сохранил.
def place_order(order_id: str) -> None:
order = orders.find(order_id)
if not order["items"]:
raise ValueError("Order is empty")
payment.charge(order["customer_id"], order["total"])
orders.update(order_id, {"status": "confirmed"})
Когда: простые домены, типовые CRUD, небольшие проекты, когда правила элементарны и почти не меняются.
Плюсы: минимум абстракций, легко читается, быстро пишется.
Минусы: сложная логика превращается в кашу из if, теряется связь с бизнесом.
Связь с DDD: это противоположность DDD; стоит выбирать его, пока бизнес-правила помещаются на пару страниц.
2. Table Module / ActiveRecord
Объект соответствует записи в таблице и совмещает данные с операциями над ними. В Python это классические Django-модели или declarative_base из SQLAlchemy.
class Order(models.Model):
status = models.CharField(max_length=20, default="created")
def confirm(self):
if not self.items.exists():
raise ValueError("Order is empty")
self.status = "confirmed"
self.save()
Когда: внутренние инструменты, админки, проекты со сроком жизни до пары лет. Плюсы: привычно, быстро, модель почти бесплатна. Минусы: домен привязан к схеме БД; сложные инварианты и агрегаты здесь не живут. Связь с DDD: часто берут как «дешёвую замену»; на сложных правилах упирается в потолок.
3. Anemic Domain Model
Объекты — «пустышки» с геттерами/сеттерами, а вся логика собрана в сервисах. По Фаулеру это антипаттерн, но в реальных проектах встречается чаще всего.
class Order:
def __init__(self):
self.status = "created" # просто поле, без методов
# вся бизнес-логика уехала в сервис:
def confirm_order(order: Order) -> None:
if not order.items:
raise ValueError("Order is empty")
order.status = "confirmed"
Когда: почти никогда осознанно — это скорее «полумера» между Transaction Script и DDD. Плюсы: проще, чем DDD, код сервисов похож на привычный. Минусы: инварианты размазаны, дублирование логики, рискованно менять. Связь с DDD: это то, во что вырождается DDD, если модель не поддерживать. Если ваши «сервисы» уже тянут на сотни строк — сигнал перейти к DDD.
4. Слоистая архитектура (Layered / N-tier)
Классическое разделение: Presentation → Application → Domain → Infrastructure. Очень близка к DDD и может применяться с ним или без него.
controllers/ # Presentation
services/ # Application
models/ # Domain
repositories/ # Infrastructure
Когда: средние проекты, когда нужно упорядочить код без полного DDD. Плюсы: понятные границы, привычная структура, домен можно «наращивать» постепенно. Минусы: при ленивом подходе слои смешиваются, и всё возвращается в контроллеры. Связь с DDD: DDD-код почти всегда укладывается в слоистую схему; это точка старта для частичного внедрения.
5. Service Layer (Application Services)
Выделение слоя прикладных сервисов, которые оркеструют use cases и управляют транзакциями, пока домен остаётся тонким.
class OrderApplicationService:
def confirm_order(self, order_id: str) -> None:
order = self.order_repo.find(order_id)
order.confirm()
self.order_repo.save(order)
Когда: нужен порядок в use cases, но без полного DDD-маппинга. Плюсы: контроллеры худеют, транзакции в одном месте, легко тестировать. Минусы: без дисциплины «умные сервисы» высасывают логику из модели. Связь с DDD: это первый шаг на пути к DDD: сервисы приложения + репозитории + чуть богаче модель.
6. Hexagonal Architecture (Ports & Adapters)
Ядро (домен) не знает ни о БД, ни об HTTP — внешний мир подключается через порты (интерфейсы) и адаптеры (реализации). Отлично сочетается с DDD.
from typing import Protocol
# Порт — контракт в терминах домена
class OrderPort(Protocol):
def find(self, order_id: str) -> Order: ...
def save(self, order: Order) -> None: ...
# Адаптер — реализация для конкретной БД
class PostgresOrderRepository(OrderPort):
def find(self, order_id: str) -> Order: ...
def save(self, order: Order) -> None: ...
Когда: тестируемость важнее скорости, частая смена инфраструктуры, микросервисы. Плюсы: домен максимально изолирован, всё легко мокается. Минусы: больше абстракций и «прокладок». Связь с DDD: практически обязательный спутник DDD в крупных системах; можно брать и отдельно.
7. CQRS (Command Query Responsibility Segregation)
Разделение на модель записи (команды меняют состояние) и модель чтения (запросы для UI/отчётов).
# Запись: команда
class ConfirmOrderCommand:
def execute(self, order_id: str) -> None:
order = self.repo.find(order_id)
order.confirm()
self.repo.save(order)
# Чтение: отдельная read-модель
class OrderReadModel:
def get_dashboard(self, customer_id: str) -> list[dict]:
return self.db.query("SELECT ... WHERE customer_id = %s", customer_id)
Когда: тяжёлое чтение, разные форматы выдачи, высокая нагрузка, совместно с Event Sourcing. Плюсы: чтение и запись оптимизируются независимо, read-модель проста. Минусы: синхронизация моделей, двойное хранение, сложность выше DDD. Связь с DDD: часто идут в паре, но CQRS можно применять без DDD и наоборот. Это НЕ «уровень DDD», а отдельное решение под нагрузку.
8. Event Sourcing
Хранение не текущего состояния, а потока событий, из которого состояние восстанавливается.
class Order:
def apply(self, event) -> None:
if isinstance(event, OrderPlaced):
self.status = "placed"
# вместо «сохранить заказ» — сохраняем события
class OrderEventStore:
def save(self, order_id: str, events: list) -> None: ...
def load(self, order_id: str) -> Order:
order = Order()
for event in self.events_for(order_id):
order.apply(event)
return order
Когда: нужен полный аудит, восстановление состояния на любую дату, сложная доменная логика. Плюсы: история — источник истины, нет потери данных, легко воспроизводить сценарии. Минусы: высокая сложность, event store, версионирование событий, обучение команды. Связь с DDD: считается «продвинутым» дополнением; не сочетается с ActiveRecord. Берите только при реальной необходимости аудита.
9. Domain Events (лёгкий вариант)
Из DDD можно взять только события — всё остальное оставить классическим MVC.
order.confirm()
self.events.append(OrderConfirmed(order.order_id, order.customer_id))
# позже — публикуем и реагируем: уведомления, склад, отчёты
Когда: изолировать побочные эффекты (почта, уведомления, интеграции) без перестройки проекта. Плюсы: малый порог входа, убирает «спагетти» из вызовов между сервисами. Минусы: без дисциплины события превращаются в «божественные» шины. Связь с DDD: лучший способ попробовать DDD на практике и получить быстрый эффект.
10. Clean Architecture
Вариация слоистой архитектуры с направлением зависимостей внутрь: бизнес-правила в центре, всё внешнее — по краям. Правила зависимостей такие же, как в Hexagonal, но с понятием слоёв use cases.
entities/ # доменные сущности
usecases/ # сценарии приложения
adapters/ # контроллеры, репозитории
frameworks/ # Django/FastAPI/БД/HTTP
Когда: те же случаи, что у Hexagonal; часто выбирается за привычность имён слоёв. Плюсы: чёткие правила зависимостей, простая навигация по проекту. Минусы: при избыточности порождает много слоёв-обёрток. Связь с DDD: отлично уживается, доменный слой ставится в центр — но DDD не требует именно её.
Сводная таблица
| Паттерн | Основная идея | Когда выбирать | Отношение к DDD |
|---|---|---|---|
| Transaction Script | Логика как шаги в одном месте | Простой CRUD, малый проект | Противоположность |
| Table Module / ActiveRecord | Объект = запись в таблице | Внутренние инструменты, админки | Дешёвая замена |
| Anemic Domain Model | Пустые объекты + «умные» сервисы | Как «полумера» (антипаттерн) | Вырожденный DDD |
| Layered Architecture | Слои Presentation→Infrastructure | Средние проекты | Основа для DDD |
| Service Layer | Оркестрация use cases в сервисах | Нужен порядок без полного DDD | Первый шаг к DDD |
| Hexagonal (Ports & Adapters) | Ядро без зависимостей от внешнего | Тестируемость, микросервисы | Спутник DDD |
| CQRS | Отдельные модели чтения и записи | Сложное чтение, нагрузка | Часто вместе, но независимо |
| Event Sourcing | Хранение событий вместо состояния | Аудит, восстановление истории | Продвинутое дополнение |
| Domain Events | Только события из DDD | Изоляция побочных эффектов | Лёгкое введение в DDD |
| Clean Architecture | Зависимости внутрь, домен в центре | Упорядочить крупный проект | Уживается с DDD |
Как внедрять DDD постепенно
Переписывать проект с нуля не нужно. Разумная стратегия «шаг за шагом»:
- Начните с Ubiquitous Language — составьте глоссарий с экспертами, исправьте термины в коде.
- Выделите один bounded context и сделайте его богатым: уберите
set_status, добавьте методы-инварианты (approve,cancel). - Добавьте репозитории и service layer для изоляции БД и транзакций.
- Подключите Domain Events для побочных эффектов.
- Только потом — агрегаты, CQRS, Event Sourcing, если реально появятся задачи.
Подробнее про сам процесс — в статье про итеративный процесс (Iterative Development).
Итог
DDD — это инструмент под сложные бизнес-правила, а не «обязательная» архитектура. Если чек-лист показал мало «Да», честно выберите Transaction Script, ActiveRecord или слоистую архитектуру — это сэкономит нервы. Если «Да» больше шести — идите в DDD и берите только то, что нужно: тактику, события, а в особо сложных случаях — CQRS и Event Sourcing.