Привет, Хабр! 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}
Что не так с этим кодом? Он работает. Но он:
Знает всё о базе данных
Смешивает бизнес-правила с SQL
Непонятно, что такое "заказ" в терминах бизнеса
При изменении правил нужно менять всё
Это типичный 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 когда:
Сложная бизнес-логика (финансы, логистика, медицина)
Часто меняются бизнес-правила
Нужна долгосрочная поддержка
Есть предметные эксперты (domain experts)
Не используйте DDD когда:
Простой CRUD (админка, блог)
Прототип или MVP
Нет экспертов предметной области
Сжатые сроки и маленькая команда
Реальный пример: система бронирования отелей
Без 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
))
Типичные ошибки новичков
Слишком много агрегатов — начинают делать агрегат для каждой таблицы
Анемичная модель — данные без поведения (getters/setters)
Игнорирование bounded contexts — одна огромная модель на всё приложение
Репозиторий на каждый объект — нарушают целостность агрегата
Практический совет с чего начать
Начните с событий — спросите: "Какие важные события происходят в системе?"
Выделите один bounded context — самый важный для бизнеса
Сделайте "толстую" модель — перенесите 2-3 бизнес-правила из сервисов в доменные объекты
Внедряйте постепенно — не нужно переписывать всё сразу
Итог
DDD — это не про сложные диаграммы и умные слова. Это про:
Говорить на языке бизнеса (единый язык — Ubiquitous Language)
Изолировать сложность (bounded contexts)
Инкапсулировать правила (в агрегатах)
Четко разделять ответственность (домен/приложение/инфраструктура)
Самый важный вопрос для проверки: Можете ли вы объяснить логику своего кода бизнес-аналитику без упоминания баз данных, API и фреймворков?
Если да — вы на пути к DDD. Если нет — у вас Data-Driven Design.
А какой подход используете вы? Сталкивались ли с попытками внедрить DDD? Делитесь опытом в комментариях!
Комментарии (20)

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

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

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

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

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

Kahelman
22.12.2025 16:53Вопрос в том кто это будет писать/ отлаживать и поддерживать. Если вы проектирует не от структур данных, то у вас через Х итераций будет полный треш. И что за что отвечает, где храниться и где у нас мастер данные понять будет невозможно.
Я Тут как раз в одном код-ферст проекте развдекаюсь - треш полный.
Если начинать проектирование с данных/ БД то потом можно на основании структуры данных генерить любые классы любым инструментом сохраняя при этом нормальную структуру.
Как только вы начинаете с другого конца- бизнес прави или чего угодно у вас будет треш.

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

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

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

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

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

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

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

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

beskov
22.12.2025 16:53Я вижу тут только чистый код, separation of concerns и ОО. Слоистая или в лучшем случае гексагональная архитектура
DDD подразумевает проектирование, в статье проектирование не показано, только то, что может быть одним из результатов
Мощнейшее добавление DDD к ООП — это более явное выделение контекстов и переопределение (перегрузка) классов в разных контекстах, в статье этого нет
VVitaly
Если эффективность, масштабируемость и производительность вашему приложению/бизнесу "не интересны", то "объектный подход" вам вполне подойдет.... :-)