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