Привет, Хабр! 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? Делитесь опытом в комментариях!