Привет, Хабр! Cегодня я хочу поговорить о самом непонятном и переоцененном термине в мире архитектуры — Domain-Driven Design (DDD). Я объясню его так, чтобы стало понятно даже джуну, и покажу на реальных примерах, чем он отличается от других подходов.

Проблема, с которой сталкивался каждый

Вы когда-нибудь видели такой код?

# order_service.py
class OrderService:
    def create_order(self, user_id, product_ids):
        # 1. Проверяем пользователя
        user = db.query("SELECT * FROM users WHERE id = %s", user_id)
        if not user:
            raise Exception("User not found")
        
        # 2. Проверяем товары
        products = []
        total = 0
        for pid in product_ids:
            product = db.query("SELECT * FROM products WHERE id = %s", pid)
            if not product:
                raise Exception(f"Product {pid} not found")
            if product["stock"] < 1:
                raise Exception(f"Product {pid} out of stock")
            products.append(product)
            total += product["price"]
        
        # 3. Проверяем баланс
        if user["balance"] < total:
            raise Exception("Insufficient funds")
        
        # 4. Создаем заказ
        order_id = db.execute("""
            INSERT INTO orders (user_id, total, status) 
            VALUES (%s, %s, 'pending')
        """, user_id, total)
        
        # 5. Добавляем позиции
        for product in products:
            db.execute("""
                INSERT INTO order_items (order_id, product_id, price)
                VALUES (%s, %s, %s)
            """, order_id, product["id"], product["price"])
            
            # 6. Обновляем склад
            db.execute("""
                UPDATE products SET stock = stock - 1 
                WHERE id = %s
            """, product["id"])
        
        # 7. Списываем деньги
        db.execute("""
            UPDATE users SET balance = balance - %s 
            WHERE id = %s
        """, total, user_id)
        
        return {"order_id": order_id}

Что не так с этим кодом? Он работает. Но он:

  1. Знает всё о базе данных

  2. Смешивает бизнес-правила с SQL

  3. Непонятно, что такое "заказ" в терминах бизнеса

  4. При изменении правил нужно менять всё

Это типичный Data-Driven Design — мы проектируем от таблиц.

DDD: переворачиваем с ног на голову

DDD предлагает начать не с таблиц, а с бизнеса. Вот как выглядит тот же процесс в DDD:

# domain/order.py
class Order:
    def __init__(self, order_id: OrderId, customer: Customer, items: list[OrderItem]):
        self.order_id = order_id
        self.customer = customer
        self.items = items
        self.status = OrderStatus.PENDING
        self.total = sum(item.price for item in items)
        
    def place(self):
        # Бизнес-правило: нельзя создать пустой заказ
        if not self.items:
            raise DomainError("Order must have at least one item")
        
        # Бизнес-правило: проверка лимита покупателя
        if not self.customer.can_purchase(self.total):
            raise DomainError("Customer purchase limit exceeded")
        
        # Бизнес-правило: проверка доступности товаров
        for item in self.items:
            if not item.product.is_available():
                raise DomainError(f"Product {item.product.name} is not available")
        
        self.status = OrderStatus.PLACED
        self.place_time = datetime.now()
        
        # Порождение domain event
        self.add_event(OrderPlacedEvent(
            order_id=self.order_id,
            customer_id=self.customer.id,
            total=self.total
        ))

Видите разницу? Этот код не знает о:

  • Базе данных

  • HTTP

  • Внешних API

  • Фреймворках

Он знает только бизнес-правила.

Ключевые строительные блоки DDD

1. Домен (Domain) — это не база данных

Это вся предметная область вашего приложения. Для Uber — это поездки, цены, водители. Для банка — счета, транзакции, клиенты.

2. Поддомен (Subdomain)

Разбиваем домен на части:

  • Core — самое важное, конкурентное преимущество (для Uber — алгоритм подбора водителей)

  • Supporting — важно, но не уникально (платежи, уведомления)

  • Generic — стандартные задачи (логирование, аутентификация)

3. Ограниченный контекст (Bounded Context)

Самая важная концепция! Это граница, внутри которой у терминов есть четкое значение.

Пример: термин "пользователь" в разных контекстах:

# Контекст "Аутентификация"
class User:
    username: str
    password_hash: str
    email: str
    is_active: bool

# Контекст "Доставка"
class Customer:
    customer_id: str
    delivery_address: Address
    preferred_time: TimeWindow
    contact_phone: str

# Контекст "Бухгалтерия"
class Client:
    tax_id: str
    billing_address: Address
    payment_terms: str
    credit_limit: Decimal

Важно: Это три разных класса! Они не связаны напрямую.

4. Агрегат (Aggregate) — транзакционная граница

Группа объектов, которые меняются как единое целое.

# Агрегат "Заказ"
class Order:
    def __init__(self, id: OrderId, customer: Customer):
        self.id = id
        self.customer = customer
        self.items: list[OrderItem] = []
        self.payments: list[Payment] = []
        self.status: OrderStatus
    
    def add_item(self, product: Product, quantity: int):
        # Правило: нельзя менять оплаченный заказ
        if self.status == OrderStatus.PAID:
            raise DomainError("Cannot modify paid order")
        
        item = OrderItem(product, quantity)
        self.items.append(item)
    
    def apply_payment(self, payment: Payment):
        self.payments.append(payment)
        
        total_paid = sum(p.amount for p in self.payments)
        if total_paid >= self.total_amount:
            self.status = OrderStatus.PAID
            self.add_event(OrderPaidEvent(order_id=self.id))

Корень агрегата (Aggregate Root) — единственная точка входа для изменений. Чтобы изменить OrderItem, нужно работать через Order.

5. Репозиторий (Repository) — абстракция над хранилищем

class OrderRepository(ABC):
    @abstractmethod
    def get(self, order_id: OrderId) -> Order:
        pass
    
    @abstractmethod
    def save(self, order: Order) -> None:
        pass

# Реализация для PostgreSQL
class PostgresOrderRepository(OrderRepository):
    def __init__(self, session):
        self.session = session
    
    def get(self, order_id: OrderId) -> Order:
        # Преобразуем данные БД в доменный объект
        data = self.session.query(OrderModel).filter_by(id=order_id).first()
        return OrderMapper.to_domain(data)
    
    def save(self, order: Order) -> None:
        # Сохраняем агрегат целиком
        data = OrderMapper.to_persistence(order)
        self.session.merge(data)
        
        # Сохраняем domain events
        for event in order.events:
            event_bus.publish(event)
        
        order.clear_events()

6. Сервис домена (Domain Service) — операция, не принадлежащая одному объекту

class PaymentProcessingService:
    def process_payment(self, order: Order, payment_method: PaymentMethod):
        # Сложная логика, затрагивающая несколько агрегатов
        payment = Payment.create(order.total, payment_method)
        
        fraud_check = self.fraud_detector.check(payment)
        if fraud_check.is_high_risk:
            order.mark_as_suspicious()
        
        payment.process()
        order.apply_payment(payment)
        
        if payment.is_successful:
            self.shipping_service.schedule_delivery(order)

Сравнение с другими подходами

DDD vs CRUD (Data-Driven)

CRUD приложение

DDD приложение

Проектируем таблицы

Проектируем доменную модель

Сервисы работают с БД напрямую

Сервисы работают с репозиториями

Бизнес-логика в сервисах

Бизнес-логика в доменных объектах

"Тонкие" модели (DTO)

"Толстые" модели с поведением

Изменение БД = изменение кода

Модель инкапсулирует сложность

DDD vs Clean/Hexagonal Architecture

  • DDD — про что моделировать (бизнес-понятия)

  • Clean/Hexagonal — про как организовать код (слои, зависимости)

Они прекрасно дополняют друг друга!

# Пример объединения DDD + Hexagonal
src/
├── domain/           # Чистая бизнес-логика (DDD)
│   ├── order.py      # Агрегаты, entities, value objects
│   ├── services.py   # Domain services
│   └── events.py     # Domain events
├── application/      # Use cases, orchestration
│   └── use_cases/
│       └── place_order.py
├── infrastructure/   # Реализации репозиториев, внешние API
│   ├── repositories/
│   │   └── postgres_order_repo.py
│   └── api_clients/
│       └── payment_gateway_client.py
└── presentation/     # Controllers, API endpoints
    └── api/
        └── order_controller.py

Когда использовать DDD?

Используйте DDD когда:

  1. Сложная бизнес-логика (финансы, логистика, медицина)

  2. Часто меняются бизнес-правила

  3. Нужна долгосрочная поддержка

  4. Есть предметные эксперты (domain experts)

Не используйте DDD когда:

  1. Простой CRUD (админка, блог)

  2. Прототип или MVP

  3. Нет экспертов предметной области

  4. Сжатые сроки и маленькая команда

Реальный пример: система бронирования отелей

Без DDD:

def book_room(user_id, room_id, dates):
    # 500 строк SQL и проверок...

С DDD:

# Домен
class Booking:
    def __init__(self, guest: Guest, room: Room, period: DateRange):
        self.guest = guest
        self.room = room
        self.period = period
        self.status = BookingStatus.PENDING
        
    def confirm(self, payment: Payment):
        if not self.room.is_available(self.period):
            raise DomainError("Room not available")
        
        if self.guest.has_overdue_bookings():
            raise DomainError("Guest has overdue bookings")
        
        if self.period.length > 30 and not payment.is_guaranteed():
            raise DomainError("Long stays require guarantee")
        
        self.status = BookingStatus.CONFIRMED
        self.confirmation_date = datetime.now()
        
        self.add_event(RoomBookedEvent(
            room_id=self.room.id,
            period=self.period,
            guest_id=self.guest.id
        ))

Типичные ошибки новичков

  1. Слишком много агрегатов — начинают делать агрегат для каждой таблицы

  2. Анемичная модель — данные без поведения (getters/setters)

  3. Игнорирование bounded contexts — одна огромная модель на всё приложение

  4. Репозиторий на каждый объект — нарушают целостность агрегата

Практический совет с чего начать

  1. Начните с событий — спросите: "Какие важные события происходят в системе?"

  2. Выделите один bounded context — самый важный для бизнеса

  3. Сделайте "толстую" модель — перенесите 2-3 бизнес-правила из сервисов в доменные объекты

  4. Внедряйте постепенно — не нужно переписывать всё сразу

Итог

DDD — это не про сложные диаграммы и умные слова. Это про:

  1. Говорить на языке бизнеса (единый язык — Ubiquitous Language)

  2. Изолировать сложность (bounded contexts)

  3. Инкапсулировать правила (в агрегатах)

  4. Четко разделять ответственность (домен/приложение/инфраструктура)

Самый важный вопрос для проверки: Можете ли вы объяснить логику своего кода бизнес-аналитику без упоминания баз данных, API и фреймворков?

Если да — вы на пути к DDD. Если нет — у вас Data-Driven Design.

А какой подход используете вы? Сталкивались ли с попытками внедрить DDD? Делитесь опытом в комментариях!

Комментарии (20)


  1. VVitaly
    22.12.2025 16:53

    Если эффективность, масштабируемость и производительность вашему приложению/бизнесу "не интересны", то "объектный подход" вам вполне подойдет.... :-)


  1. GospodinKolhoznik
    22.12.2025 16:53

    При изменении правил нужно менять всё

    При изменении правил вам так или иначе придётся менять всё. Либо переписывать код вместе с SQL запросами, либо переписывать кучу слоёв абстракции. А так-то DDD дело хорошее. Ну по крайней мере в теории.


    1. strelkove
      22.12.2025 16:53

      Зависит от того, на каком уровне правила ищменились. Если, условно, добавился новый фильтр, его придется покинуть по всем слоям, включая sql. Если же поменялось условие валидации в какой-то entity (например), то изменения будут только в домене.


      1. nihil-pro
        22.12.2025 16:53

        Только условия валидации это очень незначительно, и меняется редко, а «добавить фильтр» — наоборот.


        1. strelkove
          22.12.2025 16:53

          Условие валидации - это как пример. Возможно, что логика добавилась на слое сервиса, тогда тоже только на уровне сервиса будет изменение.


  1. Bonus2k
    22.12.2025 16:53

    Хорошее руководство для новичков, но я бы добавил про порты/интерфейсы/usecase.


    1. beskov
      22.12.2025 16:53

      Тогда это будет про гексагональную архитектуру, но всё ещё не про DDD


      1. Bonus2k
        22.12.2025 16:53

        Согласен. Я написал про них в контексте статьи для новичков как про практический способ показать, как DDD-модель жить с инфраструктурой.


  1. Kahelman
    22.12.2025 16:53

    A дальше вам надо маппить ваши сущности на базу данных а потом на rest интерфейсы, а потом все это как-то поддерживать на протяжении Х лет. Сколько лет проживает ваш. Питоновский код, а сколько базы данных? Пока счёт 1:0 в пользу баз данных. Если где и жив legacy code то только в приложении к базам данных. Это как раз проблема cobol, на котором написали кучу бизнес логики и как ее теперь на другой стек переписать никто не знает. С базами данных проблем нет - как лежали данные в таблицах и обрабатывались sql запросами так и продолжают лежать.


    1. strelkove
      22.12.2025 16:53

      Про маппинг согласен, эти преобразования на каждом слое отнимают время


      1. Kahelman
        22.12.2025 16:53

        Вопрос в том кто это будет писать/ отлаживать и поддерживать. Если вы проектирует не от структур данных, то у вас через Х итераций будет полный треш. И что за что отвечает, где храниться и где у нас мастер данные понять будет невозможно.

        Я Тут как раз в одном код-ферст проекте развдекаюсь - треш полный.

        Если начинать проектирование с данных/ БД то потом можно на основании структуры данных генерить любые классы любым инструментом сохраняя при этом нормальную структуру.

        Как только вы начинаете с другого конца- бизнес прави или чего угодно у вас будет треш.


  1. subzey
    22.12.2025 16:53

    SQL-транзакций в языке бизнеса нет, поэтому и в коде не будет?


    1. Naf2000
      22.12.2025 16:53

      Транзакции вообще-то есть, расположены они как раз в домене. А вот их реализация в DB слое. Если я все правильно понимаю


      1. strelkove
        22.12.2025 16:53

        Транзакции как правило не в домене, а в сервисе, так как транзакция предполагает изменение нескольких entity. На слое сервиса создаются репозитории для работы с базой, unit of work, в котором как раз инициализируется транзакция и репозитории. Вся логика выполняется на этом сервисе, вызывая необходимые методы репозитория, а так же вызов commit и rollback. Сама реализация commit и rollback сделана на уровне DB


        1. Kahelman
          22.12.2025 16:53

          И.е. Выкинем базу данных ГТК профи писали поддержку транзакций и напишем свою. А потом удивляемся что все валится и не работает?


          1. strelkove
            22.12.2025 16:53

            Не понял, почему выкинем базу данных, почему напишем свою поддержку транзакций? В persistense используем то, что написали эти самые профи, просто там оставляем непосредственно логику работы БД, а логика работы сервиса лежит отдельно.


            1. Kahelman
              22.12.2025 16:53

              Открываем руководство к любой базе данных. Раздел уровни изоляции транзакций. Вдумчиво читаем: что происходит какие побочные эффекты возможны. Закрываем и типа реализуем на уровне сервиса …. Транзакция это согласиюоыанные изменения данных. Если вы их от уровня БД оторвали то там уже терт знает что произойдет и никто не знает как оно на самом деле будет. Кроме того ваши сервисы скорее всего растащат по разным пользователям/ базам данные. И реализовать согласованность на уровне БД вы запаритесь. Скорее всего будет уровня «и так сойдет»


              1. strelkove
                22.12.2025 16:53

                Не понимаю, вы, например, вот такую работу с транзакциями считаете какой-то неправильной и непредсказуемой? Типа, нужно всю логику прямо в функции базы данных писать? https://go.dev/doc/database/execute-transactions


        1. subzey
          22.12.2025 16:53

          Ну вот я не вижу, где в примере о бронировании номера хоть какая-то транзакционность или просто атомарность.
          Незанятость номера проверяется, ага. А если через 1 мс она изменится потому что кто-то другой забукал номер?
          Если всё это завёрнуто в одну большую транзакцию, то где обработка нарушения ограничений при её коммите?

          Похоже, что эти безусловно красивые абстракции только помешают довести этот прототип до уровня production ready


  1. beskov
    22.12.2025 16:53

    Я вижу тут только чистый код, separation of concerns и ОО. Слоистая или в лучшем случае гексагональная архитектура

    DDD подразумевает проектирование, в статье проектирование не показано, только то, что может быть одним из результатов

    Мощнейшее добавление DDD к ООП — это более явное выделение контекстов и переопределение (перегрузка) классов в разных контекстах, в статье этого нет