Стоит ли выбирать DDD?

Хоть я и обещал себе больше не возвращаться к теме DDD, но все таки добавлю еще пару слов на эту тему. Если коротко, то не делайте из еды культа. Посмотрите по сторонам.

Далее: Пара слов про Domain-Driven Design (DDD)

Ключевой вопрос при выборе архитектуры: где источник сложности? Если сложность в технической части (большие объёмы, распределённость) — вам нужны другие инструменты. Если сложность в самих бизнес-правилах, их количестве и скорости изменений — вот тут и проявляется DDD.

Практический чек-лист из 10 вопросов

Отвечайте «Да» или «Нет». Если набрали 6+ «Да» — DDD принесёт пользу. Если меньше 4 — бегите от DDD, используйте простой CRUD + Transaction Script.

Бизнес и логика (главное)

  1. Правила меняются чаще, чем таблицы в БД?
    Новые скидки, статусы заказов, логика одобрения кредитов появляются каждые 2 недели.

  2. Одна и та же сущность (например, «Заказ») означает разное для разных отделов?
    Для бухгалтерии — это проводки, для склада — коробки, для поддержки — статус жалобы.

  3. Бизнес-эксперты используют термины, которых нет в коде?
    В коде state = 5, а в бизнесе говорят «В ожидании верификации».

  4. Для расчёта одной операции нужно собрать данные из 5+ таблиц с кучей JOIN’ов?

Команда и процесс

  1. В команде есть сеньоры, готовые обучать DDD, и бюджет на обучение?

  2. Бизнес-эксперты (не продакт-менеджеры!) готовы уделять 2+ часа в неделю на обсуждение моделей?

  3. Проект рассчитан на жизнь от 3 лет (не стартап на выброс)?

Технические триггеры

  1. У вас микросервисы, и вы ловите себя на том, что дублируете одну логику в 3 сервисах?
    Признак, что не выделены Bounded Contexts.

  2. В коде полно if (type == "A") { ... } else if (type == "B") — и их уже больше 20 штук?

  3. Вы часто правите старый код и боитесь что-то сломать, потому что логика «размазана» по контроллерам, сервисам и хелперам?

Результат

  • 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 постепенно

Переписывать проект с нуля не нужно. Разумная стратегия «шаг за шагом»:

  1. Начните с Ubiquitous Language — составьте глоссарий с экспертами, исправьте термины в коде.
  2. Выделите один bounded context и сделайте его богатым: уберите set_status, добавьте методы-инварианты (approve, cancel).
  3. Добавьте репозитории и service layer для изоляции БД и транзакций.
  4. Подключите Domain Events для побочных эффектов.
  5. Только потом — агрегаты, CQRS, Event Sourcing, если реально появятся задачи.

Подробнее про сам процесс — в статье про итеративный процесс (Iterative Development).

Итог

DDD — это инструмент под сложные бизнес-правила, а не «обязательная» архитектура. Если чек-лист показал мало «Да», честно выберите Transaction Script, ActiveRecord или слоистую архитектуру — это сэкономит нервы. Если «Да» больше шести — идите в DDD и берите только то, что нужно: тактику, события, а в особо сложных случаях — CQRS и Event Sourcing.