← все темы
DDD (Domain-Driven Design)
Вопросов с разбором: 47. В боте эти же карточки приходят по расписанию повторений.
1 Блок 1 из 7: Основы — Что такое DDD и какую проблему он решает? 🔥
DDD (Domain-Driven Design, предметно-ориентированное проектирование) — подход к разработке сложных систем, при котором в центре стоит модель предметной области, а не база данных, фреймворк или UI. Автор — Эрик Эванс («синяя книга», 2003).
Проблема: в сложном бизнесе код расходится с тем, как думают эксперты. Правила размазаны по контроллерам, ORM-моделям и SQL, а разработчики и бизнес говорят на разных языках.
Что предлагает DDD:
- общий язык с экспертами и модель, выраженная прямо в коде;
- деление большой системы на ограниченные контексты со своими моделями (стратегическое проектирование);
- строительные блоки для кода модели: сущности, объекты-значения, агрегаты, репозитории, доменные события (тактическое проектирование).
Коротко: DDD — это про то, чтобы код говорил на языке бизнеса и сложность была разложена по чётким границам.
Проблема: в сложном бизнесе код расходится с тем, как думают эксперты. Правила размазаны по контроллерам, ORM-моделям и SQL, а разработчики и бизнес говорят на разных языках.
Что предлагает DDD:
- общий язык с экспертами и модель, выраженная прямо в коде;
- деление большой системы на ограниченные контексты со своими моделями (стратегическое проектирование);
- строительные блоки для кода модели: сущности, объекты-значения, агрегаты, репозитории, доменные события (тактическое проектирование).
Коротко: DDD — это про то, чтобы код говорил на языке бизнеса и сложность была разложена по чётким границам.
2 Блок 1 из 7: Основы — Что такое предметная область и модель предметной области?
Предметная область (domain) — та часть реального мира, которую автоматизирует система: страхование, логистика, банкинг, доставка еды.
Модель предметной области — упрощённое представление домена, в котором оставлено только важное для решаемых задач: понятия, их связи и правила.
- Модель — это не схема БД и не UML-диаграмма, а общее понимание, которое выражено в коде.
- Модель не обязана быть «правильной» в абсолютном смысле — она должна быть полезной для конкретных сценариев.
- Модель развивается: по мере понимания бизнеса её уточняют и рефакторят (knowledge crunching).
Модель предметной области — упрощённое представление домена, в котором оставлено только важное для решаемых задач: понятия, их связи и правила.
- Модель — это не схема БД и не UML-диаграмма, а общее понимание, которое выражено в коде.
- Модель не обязана быть «правильной» в абсолютном смысле — она должна быть полезной для конкретных сценариев.
- Модель развивается: по мере понимания бизнеса её уточняют и рефакторят (knowledge crunching).
3 Блок 1 из 7: Основы — Что такое единый язык (Ubiquitous Language)? 🔥
Ubiquitous Language — общий язык разработчиков и экспертов предметной области, который используется везде: в разговорах, документации, задачах и в коде.
- Если бизнес говорит «оформить полис», в коде метод
- Термины однозначны внутри одного ограниченного контекста: «Клиент» в продажах и «Клиент» в поддержке могут быть разными понятиями.
- Расхождение языка в коде и в разговоре — сигнал, что модель устарела или понята неверно.
- Если бизнес говорит «оформить полис», в коде метод
policy.issue(), а не update_status(3).- Термины однозначны внутри одного ограниченного контекста: «Клиент» в продажах и «Клиент» в поддержке могут быть разными понятиями.
- Расхождение языка в коде и в разговоре — сигнал, что модель устарела или понята неверно.
# плохо: технический язык
order.status = 4
order.save()
# хорошо: язык бизнеса
order.cancel(reason=CancelReason.OUT_OF_STOCK)
4 Блок 1 из 7: Основы — Когда DDD оправдан, а когда избыточен? 🔥
Оправдан:
- сложная, нетривиальная бизнес-логика с большим количеством правил и исключений;
- домен — источник конкурентного преимущества компании (core domain);
- долгоживущий продукт, который будет развиваться годами;
- есть доступ к экспертам предметной области.
Избыточен:
- CRUD: формы, админки, справочники — логики почти нет, данные просто сохраняются;
- прототипы и короткоживущие проекты;
- технически сложные, но бедные бизнес-правилами системы (прокси, ETL, интеграции).
Важно: стратегические идеи (единый язык, ограниченные контексты) полезны почти везде; тяжёлую тактику (агрегаты, репозитории, события) стоит применять только к сложным частям системы.
- сложная, нетривиальная бизнес-логика с большим количеством правил и исключений;
- домен — источник конкурентного преимущества компании (core domain);
- долгоживущий продукт, который будет развиваться годами;
- есть доступ к экспертам предметной области.
Избыточен:
- CRUD: формы, админки, справочники — логики почти нет, данные просто сохраняются;
- прототипы и короткоживущие проекты;
- технически сложные, но бедные бизнес-правилами системы (прокси, ETL, интеграции).
Важно: стратегические идеи (единый язык, ограниченные контексты) полезны почти везде; тяжёлую тактику (агрегаты, репозитории, события) стоит применять только к сложным частям системы.
5 Блок 1 из 7: Основы — Что такое анемичная модель и чем она отличается от богатой? 🔥
Анемичная модель — объекты содержат только данные (поля, геттеры/сеттеры), а вся логика лежит в «сервисах». Мартин Фаулер называет это анти-паттерном для сложного домена.
Богатая модель — объект сам отвечает за свои правила: данные и поведение вместе, состояние меняется только через методы, которые проверяют инварианты.
Итог: анемичная модель нормальна для CRUD, но в сложном домене ведёт к дублированию правил и нарушенным инвариантам.
Богатая модель — объект сам отвечает за свои правила: данные и поведение вместе, состояние меняется только через методы, которые проверяют инварианты.
# анемичная: правило живёт снаружи и легко обходится
if order.status == 'new' and order.total > 0:
order.status = 'paid'
# богатая: правило внутри объекта
class Order:
def pay(self, amount):
if self.status != Status.NEW:
raise OrderAlreadyPaid(self.id)
if amount != self.total:
raise WrongPaymentAmount(amount, self.total)
self.status = Status.PAID
Итог: анемичная модель нормальна для CRUD, но в сложном домене ведёт к дублированию правил и нарушенным инвариантам.
6 Блок 2 из 7: Стратегическое проектирование — Что такое ограниченный контекст (Bounded Context)? 🔥
Bounded Context — явная граница, внутри которой действует одна модель и один единый язык. Термины внутри контекста однозначны.
Пример: понятие «Товар».
- В контексте Каталог — название, описание, фото, характеристики.
- В контексте Склад — SKU, остатки, ячейка хранения, вес.
- В контексте Доставка — габариты и хрупкость.
Вместо одного гигантского класса
Граница обычно совпадает с модулем, сервисом, репозиторием кода или командой. Между контекстами данные передаются через явные контракты (API, события), а не через общие таблицы.
Пример: понятие «Товар».
- В контексте Каталог — название, описание, фото, характеристики.
- В контексте Склад — SKU, остатки, ячейка хранения, вес.
- В контексте Доставка — габариты и хрупкость.
Вместо одного гигантского класса
Product на все случаи — несколько маленьких моделей, каждая в своём контексте.Граница обычно совпадает с модулем, сервисом, репозиторием кода или командой. Между контекстами данные передаются через явные контракты (API, события), а не через общие таблицы.
7 Блок 2 из 7: Стратегическое проектирование — Как найти границы ограниченных контекстов?
Признаки, по которым проводят границы:
- Язык меняется: одно слово значит разное («Счёт» у бухгалтерии и у клиента) — это разные контексты.
- Разные эксперты и подразделения: склад, бухгалтерия, маркетинг думают о домене по-разному.
- Бизнес-процессы и их этапы: оформление заказа → оплата → комплектация → доставка.
- Разная скорость изменений и разные требования к надёжности/нагрузке.
- Команды: контекст удобно отдавать одной команде (закон Конвея).
Инструмент: Event Storming — команда и эксперты выкладывают доменные события на таймлайн, и границы проявляются там, где меняется язык и ответственность.
Ошибка: резать по техническим слоям или по таблицам («сервис пользователей», «сервис заказов-CRUD»).
- Язык меняется: одно слово значит разное («Счёт» у бухгалтерии и у клиента) — это разные контексты.
- Разные эксперты и подразделения: склад, бухгалтерия, маркетинг думают о домене по-разному.
- Бизнес-процессы и их этапы: оформление заказа → оплата → комплектация → доставка.
- Разная скорость изменений и разные требования к надёжности/нагрузке.
- Команды: контекст удобно отдавать одной команде (закон Конвея).
Инструмент: Event Storming — команда и эксперты выкладывают доменные события на таймлайн, и границы проявляются там, где меняется язык и ответственность.
Ошибка: резать по техническим слоям или по таблицам («сервис пользователей», «сервис заказов-CRUD»).
8 Блок 2 из 7: Стратегическое проектирование — Что такое поддомены: core, supporting, generic? 🔥
Домен делят на поддомены по ценности для бизнеса:
- Core (основной) — то, чем компания отличается от конкурентов и зарабатывает. Например, ценообразование у маркетплейса или скоринг у банка. Сюда вкладывают лучших людей и полноценный DDD.
- Supporting (вспомогательный) — нужен бизнесу, но не даёт преимущества: управление контентом, внутренние справочники. Пишут проще, можно отдать на аутсорс.
- Generic (типовой) — решённая задача, одинаковая у всех: аутентификация, платежи, рассылки, бухгалтерия. Покупают или берут готовое (Keycloak, Stripe).
Смысл: распределять усилия. Тратить DDD и сеньоров на generic — расточительно, писать core «на коленке» — опасно.
- Core (основной) — то, чем компания отличается от конкурентов и зарабатывает. Например, ценообразование у маркетплейса или скоринг у банка. Сюда вкладывают лучших людей и полноценный DDD.
- Supporting (вспомогательный) — нужен бизнесу, но не даёт преимущества: управление контентом, внутренние справочники. Пишут проще, можно отдать на аутсорс.
- Generic (типовой) — решённая задача, одинаковая у всех: аутентификация, платежи, рассылки, бухгалтерия. Покупают или берут готовое (Keycloak, Stripe).
Смысл: распределять усилия. Тратить DDD и сеньоров на generic — расточительно, писать core «на коленке» — опасно.
9 Блок 2 из 7: Стратегическое проектирование — Чем поддомен отличается от ограниченного контекста?
- Поддомен — часть пространства задачи (problem space): как устроен бизнес. Существует независимо от нашего софта.
- Ограниченный контекст — часть пространства решения (solution space): граница модели в нашем коде.
В идеале они соотносятся 1:1, но на практике бывает иначе:
- один поддомен реализован в нескольких контекстах (легаси + новый сервис);
- один контекст покрывает несколько маленьких поддоменов.
Коротко: поддомены мы находим в бизнесе, контексты мы проектируем в системе.
- Ограниченный контекст — часть пространства решения (solution space): граница модели в нашем коде.
В идеале они соотносятся 1:1, но на практике бывает иначе:
- один поддомен реализован в нескольких контекстах (легаси + новый сервис);
- один контекст покрывает несколько маленьких поддоменов.
Коротко: поддомены мы находим в бизнесе, контексты мы проектируем в системе.
10 Блок 2 из 7: Стратегическое проектирование — Что такое карта контекстов (Context Map)?
Context Map — схема всех ограниченных контекстов системы и отношений между ними: кто от кого зависит, кто чью модель диктует, как идёт интеграция.
Зачем:
- видно, где модели «протекают» друг в друга;
- понятны организационные зависимости: какая команда upstream (влияет), какая downstream (зависит);
- помогает решить, где нужен переводчик (ACL), а где можно просто подстроиться.
Upstream — поставщик модели/данных, его изменения влияют на других. Downstream — потребитель, зависит от upstream.
Зачем:
- видно, где модели «протекают» друг в друга;
- понятны организационные зависимости: какая команда upstream (влияет), какая downstream (зависит);
- помогает решить, где нужен переводчик (ACL), а где можно просто подстроиться.
Upstream — поставщик модели/данных, его изменения влияют на других. Downstream — потребитель, зависит от upstream.
11 Блок 2 из 7: Стратегическое проектирование — Какие паттерны взаимодействия контекстов бывают?
- Partnership — две команды координируют изменения и успешны или проваливаются вместе.
- Shared Kernel — общая часть модели/кода у двух контекстов. Экономит дублирование, но связывает команды.
- Customer/Supplier — downstream (клиент) влияет на приоритеты upstream (поставщика).
- Conformist — downstream просто принимает модель upstream как есть (например, внешнего API), без перевода.
- Anti-Corruption Layer (ACL) — downstream ставит слой-переводчик, чтобы чужая модель не испортила свою.
- Open Host Service (OHS) — upstream предоставляет стабильный публичный API для многих потребителей.
- Published Language — общий документированный формат обмена (JSON Schema, Protobuf, отраслевой стандарт).
- Separate Ways — интеграция не нужна, контексты живут независимо.
- Shared Kernel — общая часть модели/кода у двух контекстов. Экономит дублирование, но связывает команды.
- Customer/Supplier — downstream (клиент) влияет на приоритеты upstream (поставщика).
- Conformist — downstream просто принимает модель upstream как есть (например, внешнего API), без перевода.
- Anti-Corruption Layer (ACL) — downstream ставит слой-переводчик, чтобы чужая модель не испортила свою.
- Open Host Service (OHS) — upstream предоставляет стабильный публичный API для многих потребителей.
- Published Language — общий документированный формат обмена (JSON Schema, Protobuf, отраслевой стандарт).
- Separate Ways — интеграция не нужна, контексты живут независимо.
12 Блок 2 из 7: Стратегическое проектирование — Что такое Anti-Corruption Layer и когда он нужен? 🔥
ACL — слой-переводчик на границе контекста, который превращает модель внешней системы в модель нашего домена и обратно. Защищает домен от чужих понятий, кривых названий и изменений внешнего API.
Когда нужен: интеграция с легаси, внешним сервисом (платёжка, CRM) или контекстом с неудобной моделью.
Итог: домен не знает про
Когда нужен: интеграция с легаси, внешним сервисом (платёжка, CRM) или контекстом с неудобной моделью.
class CrmCustomerTranslator:
"""ACL: CRM отдаёт свой формат, домен видит только Customer."""
def __init__(self, crm_client):
self._crm = crm_client
def get_customer(self, customer_id):
raw = self._crm.fetch_client(customer_id)
return Customer(
id=CustomerId(raw['ClientID']),
email=Email(raw['ContactInfo']['EMail']),
is_vip=raw['SegmentCode'] in ('VIP', 'GOLD'),
)
Итог: домен не знает про
ClientID и SegmentCode. Если CRM поменяет формат, правится только переводчик.
13 Блок 2 из 7: Стратегическое проектирование — Как ограниченные контексты соотносятся с микросервисами? 🔥
- Ограниченный контекст — хороший кандидат на границу микросервиса: у него своя модель, свой язык и свои данные.
- Но это не одно и то же: контекст — логическая граница модели, микросервис — единица развёртывания.
Варианты:
- 1 контекст = 1 сервис — самый частый и понятный случай;
- 1 контекст = несколько сервисов — когда части контекста нужно масштабировать отдельно;
- несколько контекстов в 1 сервисе — модульный монолит, контексты разделены модулями внутри одного приложения.
Практика: часто разумно начать с модульного монолита с чёткими границами контекстов и выносить сервисы, когда появится реальная причина. Сервис, нарезанный не по контексту, превращается в распределённый монолит.
- Но это не одно и то же: контекст — логическая граница модели, микросервис — единица развёртывания.
Варианты:
- 1 контекст = 1 сервис — самый частый и понятный случай;
- 1 контекст = несколько сервисов — когда части контекста нужно масштабировать отдельно;
- несколько контекстов в 1 сервисе — модульный монолит, контексты разделены модулями внутри одного приложения.
Практика: часто разумно начать с модульного монолита с чёткими границами контекстов и выносить сервисы, когда появится реальная причина. Сервис, нарезанный не по контексту, превращается в распределённый монолит.
14 Блок 3 из 7: Тактические строительные блоки — Что такое сущность (Entity)? 🔥
Entity — объект, который определяется идентичностью, а не значениями атрибутов. Имеет жизненный цикл, а его состояние меняется со временем.
- Два пользователя с одинаковым именем — разные люди: у них разные
- Пользователь сменил email — это всё тот же пользователь.
- Сравнение — по идентификатору.
- Два пользователя с одинаковым именем — разные люди: у них разные
id.- Пользователь сменил email — это всё тот же пользователь.
- Сравнение — по идентификатору.
class User:
def __init__(self, user_id, email):
self.id = user_id
self.email = email
def change_email(self, new_email):
self.email = new_email
def __eq__(self, other):
return isinstance(other, User) and self.id == other.id
def __hash__(self):
return hash(self.id)
15 Блок 3 из 7: Тактические строительные блоки — Что такое объект-значение (Value Object)? 🔥
Value Object — объект без идентичности, который определяется только значениями атрибутов: деньги, адрес, диапазон дат, email.
Свойства:
- неизменяемый (immutable) — «изменение» создаёт новый объект;
- сравнение по значению: 100 RUB == 100 RUB;
- самовалидация: невалидный объект нельзя создать;
- содержит связанное поведение (сложить деньги, проверить вхождение даты в диапазон).
Зачем: вместо «голых»
Свойства:
- неизменяемый (immutable) — «изменение» создаёт новый объект;
- сравнение по значению: 100 RUB == 100 RUB;
- самовалидация: невалидный объект нельзя создать;
- содержит связанное поведение (сложить деньги, проверить вхождение даты в диапазон).
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
def __post_init__(self):
if self.amount < 0:
raise ValueError('Сумма не может быть отрицательной')
def __add__(self, other):
if other.currency != self.currency:
raise ValueError('Разные валюты')
return Money(self.amount + other.amount, self.currency)
Зачем: вместо «голых»
Decimal и str правила живут в типе — борьба с primitive obsession.
16 Блок 3 из 7: Тактические строительные блоки — Как решить, делать объект сущностью или объектом-значением?
Главный вопрос: важно ли бизнесу отличать два объекта с одинаковыми данными?
- Да, нужно отслеживать «тот же самый» объект во времени → Entity.
- Нет, важны только значения, и их можно взаимозаменять → Value Object.
Примеры:
- Купюра в кошельке — Value Object (любые 100 рублей равны). Купюра в системе ЦБ с учётом серийных номеров — Entity.
- Адрес доставки в заказе — обычно Value Object. Адрес как объект с историей проверок в логистике — может быть Entity.
Правило по умолчанию: предпочитайте Value Object — он проще, неизменяем и безопасен. Сущностей в модели обычно меньше, чем кажется.
- Да, нужно отслеживать «тот же самый» объект во времени → Entity.
- Нет, важны только значения, и их можно взаимозаменять → Value Object.
Примеры:
- Купюра в кошельке — Value Object (любые 100 рублей равны). Купюра в системе ЦБ с учётом серийных номеров — Entity.
- Адрес доставки в заказе — обычно Value Object. Адрес как объект с историей проверок в логистике — может быть Entity.
Правило по умолчанию: предпочитайте Value Object — он проще, неизменяем и безопасен. Сущностей в модели обычно меньше, чем кажется.
17 Блок 3 из 7: Тактические строительные блоки — Что такое агрегат и корень агрегата? 🔥
Агрегат — кластер связанных сущностей и объектов-значений, который рассматривается как единое целое при изменении данных. Это граница согласованности: внутри агрегата инварианты соблюдаются всегда.
Корень агрегата (Aggregate Root) — единственная сущность, через которую внешний мир работает с агрегатом.
- Снаружи можно держать ссылку только на корень.
- Изменения внутренних объектов — только через методы корня.
- Агрегат загружается и сохраняется целиком.
Никто не делает
Корень агрегата (Aggregate Root) — единственная сущность, через которую внешний мир работает с агрегатом.
- Снаружи можно держать ссылку только на корень.
- Изменения внутренних объектов — только через методы корня.
- Агрегат загружается и сохраняется целиком.
class Order: # корень агрегата
def __init__(self, order_id):
self.id = order_id
self._lines = [] # OrderLine — внутренняя часть агрегата
def add_line(self, product_id, qty, price):
if len(self._lines) >= 50:
raise TooManyLines()
self._lines.append(OrderLine(product_id, qty, price))
@property
def total(self):
return sum((line.subtotal for line in self._lines), Money.zero())
Никто не делает
order._lines.append(...) напрямую — иначе лимит в 50 позиций можно обойти.
18 Блок 3 из 7: Тактические строительные блоки — Какие правила проектирования агрегатов? 🔥
Четыре правила Вона Вернона («Implementing DDD»):
1. Защищайте истинные инварианты внутри границы агрегата. В агрегат входит то, что должно быть согласовано немедленно, в одной транзакции.
2. Проектируйте маленькие агрегаты. Большой агрегат — это долгая загрузка, конфликты при параллельных изменениях и лишние блокировки.
3. Ссылайтесь на другие агрегаты только по идентификатору, а не объектной ссылкой.
4. Вне границы используйте eventual consistency. Согласованность между агрегатами — через доменные события, а не одной транзакцией.
Плюс правило транзакций: одна транзакция изменяет один агрегат.
1. Защищайте истинные инварианты внутри границы агрегата. В агрегат входит то, что должно быть согласовано немедленно, в одной транзакции.
2. Проектируйте маленькие агрегаты. Большой агрегат — это долгая загрузка, конфликты при параллельных изменениях и лишние блокировки.
3. Ссылайтесь на другие агрегаты только по идентификатору, а не объектной ссылкой.
4. Вне границы используйте eventual consistency. Согласованность между агрегатами — через доменные события, а не одной транзакцией.
Плюс правило транзакций: одна транзакция изменяет один агрегат.
19 Блок 3 из 7: Тактические строительные блоки — Почему агрегаты ссылаются друг на друга только по идентификатору?
# плохо: объектная ссылка на чужой агрегат
class Order:
customer: Customer
# хорошо: ссылка по id
class Order:
customer_id: CustomerId
Причины:
- Чёткая граница: нельзя случайно изменить
Customer через order.customer.block() в транзакции заказа.- Меньше загрузки: не тянется граф объектов и не возникают ленивые запросы (N+1).
- Масштабирование: агрегаты можно хранить в разных БД и сервисах.
- Меньше конфликтов: изменение клиента не конфликтует с изменением заказа.
Нужны данные другого агрегата — загрузите его отдельно через его репозиторий в прикладном сервисе.
20 Блок 3 из 7: Тактические строительные блоки — Что такое инвариант и где его проверять?
Инвариант — бизнес-правило, которое должно быть истинным всегда, после любой операции. Например: «сумма оплат не превышает сумму заказа», «у оплаченного заказа нельзя менять состав», «бронирования не пересекаются».
Где проверять:
- в конструкторе/фабрике — нельзя создать невалидный объект;
- в методах агрегата, которые меняют состояние;
- в Value Object — правила формата (email, положительная сумма).
Где не проверять: в контроллере, сериализаторе или форме. Там можно проверять формат входных данных, но не бизнес-правила — иначе другой вход (команда, консьюмер, админка) их обойдёт.
Правило, затрагивающее несколько агрегатов (например, уникальность email среди всех пользователей), — это уже не инвариант одного агрегата. Его проверяют доменным сервисом, ограничением БД или обеспечивают через eventual consistency.
Где проверять:
- в конструкторе/фабрике — нельзя создать невалидный объект;
- в методах агрегата, которые меняют состояние;
- в Value Object — правила формата (email, положительная сумма).
Где не проверять: в контроллере, сериализаторе или форме. Там можно проверять формат входных данных, но не бизнес-правила — иначе другой вход (команда, консьюмер, админка) их обойдёт.
Правило, затрагивающее несколько агрегатов (например, уникальность email среди всех пользователей), — это уже не инвариант одного агрегата. Его проверяют доменным сервисом, ограничением БД или обеспечивают через eventual consistency.
21 Блок 3 из 7: Тактические строительные блоки — Что такое доменный сервис и чем он отличается от сервиса приложения? 🔥
Доменный сервис (Domain Service) — бизнес-логика, которая не принадлежит естественно ни одной сущности: операция над несколькими агрегатами или расчёт по доменным правилам. Без состояния, на языке домена.
Пример:
Сервис приложения (Application Service) — оркестратор сценария (use case). Бизнес-логики в нём нет, он:
- загружает агрегаты из репозиториев;
- вызывает методы домена;
- управляет транзакцией и сохранением;
- публикует события, проверяет права, логирует.
Проверка: если в сервисе есть
Пример:
TransferService.transfer(from_account, to_account, money), PricingService.calculate(order, customer).Сервис приложения (Application Service) — оркестратор сценария (use case). Бизнес-логики в нём нет, он:
- загружает агрегаты из репозиториев;
- вызывает методы домена;
- управляет транзакцией и сохранением;
- публикует события, проверяет права, логирует.
Проверка: если в сервисе есть
if про бизнес-правило — скорее всего, это логика домена, которую надо перенести в агрегат или доменный сервис.
22 Блок 3 из 7: Тактические строительные блоки — Что такое фабрика (Factory) в DDD?
Фабрика инкапсулирует создание сложного агрегата, когда конструктора недостаточно: нужно собрать много частей, сгенерировать идентификатор, применить правила создания.
Формы:
- фабричный метод на классе —
- метод другого агрегата —
- отдельный класс-фабрика, если создание зависит от внешних данных.
Фабрика гарантирует, что агрегат рождается в валидном состоянии.
Формы:
- фабричный метод на классе —
Order.create(customer_id, cart);- метод другого агрегата —
forum.start_discussion(author, subject) возвращает новую Discussion;- отдельный класс-фабрика, если создание зависит от внешних данных.
class Order:
@classmethod
def create(cls, customer_id, cart):
if cart.is_empty():
raise EmptyCart()
order = cls(OrderId.new(), customer_id)
for item in cart.items:
order.add_line(item.product_id, item.qty, item.price)
order._record(OrderPlaced(order.id, customer_id))
return order
Фабрика гарантирует, что агрегат рождается в валидном состоянии.
23 Блок 3 из 7: Тактические строительные блоки — Что такое репозиторий (Repository) в DDD? 🔥
Репозиторий — абстракция хранилища, которая выглядит для домена как коллекция агрегатов в памяти: «положи», «достань по id». Домен не знает, что за ним стоит: PostgreSQL, Mongo или словарь в тесте.
Правила:
- один репозиторий на агрегат (на корень), не на каждую таблицу;
- возвращает и сохраняет агрегат целиком;
- интерфейс (порт) лежит в домене, реализация — в инфраструктуре.
В тестах подставляется
Правила:
- один репозиторий на агрегат (на корень), не на каждую таблицу;
- возвращает и сохраняет агрегат целиком;
- интерфейс (порт) лежит в домене, реализация — в инфраструктуре.
class OrderRepository(Protocol):
def get(self, order_id: OrderId) -> Order: ...
def add(self, order: Order) -> None: ...
class SqlAlchemyOrderRepository:
def __init__(self, session):
self._session = session
def get(self, order_id):
return self._session.get(Order, order_id)
def add(self, order):
self._session.add(order)
В тестах подставляется
InMemoryOrderRepository на словаре — без БД.
24 Блок 3 из 7: Тактические строительные блоки — Чем репозиторий DDD отличается от DAO или Django Manager?
- DAO (Data Access Object) ориентирован на таблицы: CRUD по строкам, часто один DAO на таблицу, возвращает записи/DTO.
- Репозиторий ориентирован на агрегаты: работает только с корнями, собирает агрегат целиком, говорит на языке домена (
- Django Manager / QuerySet — Active Record: модель сама знает про БД, запрос можно построить откуда угодно, и любой код может поменять любое поле и вызвать
На практике репозиторий может быть реализован поверх DAO, ORM или QuerySet — разница в контракте: наружу торчат доменные объекты и доменные операции, а не таблицы и произвольные запросы.
- Репозиторий ориентирован на агрегаты: работает только с корнями, собирает агрегат целиком, говорит на языке домена (
get_active_by_customer, а не select_where).- Django Manager / QuerySet — Active Record: модель сама знает про БД, запрос можно построить откуда угодно, и любой код может поменять любое поле и вызвать
save(). Границ агрегата нет.На практике репозиторий может быть реализован поверх DAO, ORM или QuerySet — разница в контракте: наружу торчат доменные объекты и доменные операции, а не таблицы и произвольные запросы.
25 Блок 4 из 7: Доменные события — Что такое доменное событие (Domain Event)? 🔥
Доменное событие — факт, который уже произошёл в домене и важен для бизнеса. Называется глаголом в прошедшем времени:
Свойства:
- неизменяемое, содержит всё нужное подписчикам (id агрегата, ключевые данные, время);
- генерируется агрегатом при изменении состояния;
- на него реагируют другие части системы.
Зачем:
- развязать побочные эффекты: заказ не должен знать про email-рассылку и бонусы;
- обеспечить согласованность между агрегатами и контекстами (eventual consistency);
- аудит и основа для Event Sourcing.
OrderPlaced, PaymentReceived, SubscriptionExpired.Свойства:
- неизменяемое, содержит всё нужное подписчикам (id агрегата, ключевые данные, время);
- генерируется агрегатом при изменении состояния;
- на него реагируют другие части системы.
@dataclass(frozen=True)
class OrderPlaced:
order_id: OrderId
customer_id: CustomerId
total: Money
occurred_at: datetime
Зачем:
- развязать побочные эффекты: заказ не должен знать про email-рассылку и бонусы;
- обеспечить согласованность между агрегатами и контекстами (eventual consistency);
- аудит и основа для Event Sourcing.
26 Блок 4 из 7: Доменные события — Как агрегат публикует доменные события?
Частый и надёжный способ — агрегат копит события, а прикладной слой забирает и отправляет их после успешного сохранения.
Почему не публиковать сразу из метода: транзакция может откатиться, а событие уже уйдёт. Ещё агрегат станет зависеть от инфраструктуры (шины).
Для гарантированной доставки события сохраняют в той же транзакции в таблицу outbox.
class AggregateRoot:
def __init__(self):
self._events = []
def _record(self, event):
self._events.append(event)
def pull_events(self):
events, self._events = self._events, []
return events
class Order(AggregateRoot):
def pay(self, payment):
...
self.status = Status.PAID
self._record(OrderPaid(self.id, payment.amount))
# прикладной сервис
with uow:
order = uow.orders.get(order_id)
order.pay(payment)
uow.commit()
for event in order.pull_events():
bus.publish(event)
Почему не публиковать сразу из метода: транзакция может откатиться, а событие уже уйдёт. Ещё агрегат станет зависеть от инфраструктуры (шины).
Для гарантированной доставки события сохраняют в той же транзакции в таблицу outbox.
27 Блок 4 из 7: Доменные события — Чем доменное событие отличается от интеграционного?
- Доменное событие — внутри одного ограниченного контекста. Обрабатывается часто в том же процессе, может содержать доменные типы, его форма меняется вместе с моделью.
- Интеграционное событие — для других контекстов и сервисов, уходит через брокер (Kafka, RabbitMQ). Это публичный контракт: стабильная схема, версионирование, только примитивы и нужные потребителям поля.
Ошибка: публиковать доменные события наружу как есть. Тогда любой рефакторинг модели ломает чужие сервисы.
- Интеграционное событие — для других контекстов и сервисов, уходит через брокер (Kafka, RabbitMQ). Это публичный контракт: стабильная схема, версионирование, только примитивы и нужные потребителям поля.
OrderPaid (доменное, внутри «Заказов»)
→ обработчик внутри контекста
→ публикует OrderPaidV1 (интеграционное) в Kafka
→ «Склад» резервирует товар, «Лояльность» начисляет баллы
Ошибка: публиковать доменные события наружу как есть. Тогда любой рефакторинг модели ломает чужие сервисы.
28 Блок 4 из 7: Доменные события — Как гарантировать, что событие отправится, если транзакция закоммитилась?
Проблема dual write: сохранить агрегат в БД и отправить событие в брокер — две разные системы. Между ними процесс может упасть, и тогда или событие потеряется, или уйдёт событие о том, чего не было.
Решение — Transactional Outbox:
1. В той же транзакции, что и изменение агрегата, событие пишется в таблицу
2. Отдельный процесс (relay) читает
3. Гарантия — at-least-once, поэтому потребители должны быть идемпотентными (дедупликация по
Решение — Transactional Outbox:
1. В той же транзакции, что и изменение агрегата, событие пишется в таблицу
outbox.2. Отдельный процесс (relay) читает
outbox и публикует в брокер, затем помечает запись отправленной. Вместо relay можно использовать CDC (Debezium).3. Гарантия — at-least-once, поэтому потребители должны быть идемпотентными (дедупликация по
event_id).with uow:
order.pay(payment)
uow.orders.add(order)
uow.outbox.add_all(order.pull_events()) # та же транзакция
uow.commit()
29 Блок 5 из 7: Архитектура приложения — Какие слои выделяют в DDD-приложении? 🔥
Классическая слоистая архитектура Эванса:
- Presentation / Interface — HTTP-контроллеры, CLI, консьюмеры сообщений. Принимают запрос, валидируют формат, вызывают прикладной слой.
- Application — сценарии использования (use cases): транзакции, загрузка агрегатов, вызов домена, публикация событий. Тонкий, без бизнес-правил.
- Domain — сущности, объекты-значения, агрегаты, доменные сервисы, события, интерфейсы репозиториев. Сердце системы.
- Infrastructure — ORM, реализации репозиториев, брокеры, внешние API, email.
Главное правило: домен не зависит ни от чего — ни от фреймворка, ни от ORM, ни от БД. Зависимости направлены к домену: инфраструктура реализует интерфейсы, объявленные в домене (инверсия зависимостей).
- Presentation / Interface — HTTP-контроллеры, CLI, консьюмеры сообщений. Принимают запрос, валидируют формат, вызывают прикладной слой.
- Application — сценарии использования (use cases): транзакции, загрузка агрегатов, вызов домена, публикация событий. Тонкий, без бизнес-правил.
- Domain — сущности, объекты-значения, агрегаты, доменные сервисы, события, интерфейсы репозиториев. Сердце системы.
- Infrastructure — ORM, реализации репозиториев, брокеры, внешние API, email.
Главное правило: домен не зависит ни от чего — ни от фреймворка, ни от ORM, ни от БД. Зависимости направлены к домену: инфраструктура реализует интерфейсы, объявленные в домене (инверсия зависимостей).
30 Блок 5 из 7: Архитектура приложения — Что такое гексагональная архитектура (Ports and Adapters)? 🔥
Гексагональная архитектура (Алистер Кокберн) — приложение и домен в центре, внешний мир подключается через порты и адаптеры.
- Порт — интерфейс, объявленный приложением:
- Адаптер — реализация порта под конкретную технологию:
- Входящие (driving) адаптеры вызывают приложение: REST, CLI, консьюмер Kafka.
- Исходящие (driven) адаптеры вызываются приложением: БД, брокер, внешние API.
Что даёт:
- домен тестируется без БД и HTTP — подставляются фейковые адаптеры;
- смена технологии (Postgres → Mongo, REST → gRPC) не трогает домен;
- естественно сочетается с DDD: домен в центре, инфраструктура по краям.
- Порт — интерфейс, объявленный приложением:
OrderRepository, PaymentGateway, Notifier.- Адаптер — реализация порта под конкретную технологию:
PostgresOrderRepository, StripeGateway, TelegramNotifier.- Входящие (driving) адаптеры вызывают приложение: REST, CLI, консьюмер Kafka.
- Исходящие (driven) адаптеры вызываются приложением: БД, брокер, внешние API.
Что даёт:
- домен тестируется без БД и HTTP — подставляются фейковые адаптеры;
- смена технологии (Postgres → Mongo, REST → gRPC) не трогает домен;
- естественно сочетается с DDD: домен в центре, инфраструктура по краям.
31 Блок 5 из 7: Архитектура приложения — Что такое Onion и Clean Architecture и правило зависимостей?
Onion (Джеффри Палермо) и Clean Architecture (Роберт Мартин) — вариации той же идеи, что и гексагональная архитектура: концентрические слои с доменом в центре.
Правило зависимостей (Dependency Rule): зависимости в коде направлены только внутрь. Внутренний слой ничего не знает о внешних: в доменном модуле нет
Когда внутреннему слою нужно что-то внешнее (сохранить, отправить), он объявляет интерфейс, а внешний слой его реализует — Dependency Inversion.
Frameworks & Drivers (БД, веб, UI)
└─ Interface Adapters (контроллеры, репозитории)
└─ Application / Use Cases
└─ Domain / Entities
Правило зависимостей (Dependency Rule): зависимости в коде направлены только внутрь. Внутренний слой ничего не знает о внешних: в доменном модуле нет
import django, import sqlalchemy или import requests.Когда внутреннему слою нужно что-то внешнее (сохранить, отправить), он объявляет интерфейс, а внешний слой его реализует — Dependency Inversion.
32 Блок 5 из 7: Архитектура приложения — Что такое Unit of Work?
Unit of Work — паттерн, который отслеживает изменения в рамках бизнес-операции и фиксирует их атомарно, одной транзакцией. Даёт прикладному слою единую точку
В SQLAlchemy
commit/rollback и доступ к репозиториям.class SqlAlchemyUnitOfWork:
def __init__(self, session_factory):
self._session_factory = session_factory
def __enter__(self):
self.session = self._session_factory()
self.orders = SqlAlchemyOrderRepository(self.session)
return self
def __exit__(self, exc_type, exc, tb):
self.session.rollback() # без явного commit ничего не сохранится
self.session.close()
def commit(self):
self.session.commit()
В SQLAlchemy
Session сама по себе реализует Unit of Work. Обёртка нужна, чтобы прикладной слой не зависел от SQLAlchemy и чтобы в тестах подставлять FakeUnitOfWork.
33 Блок 5 из 7: Архитектура приложения — Как выглядит прикладной сервис (use case) на Python? 🔥
Прикладной сервис — тонкий оркестратор: загрузить, вызвать домен, сохранить, отдать события.
Контроллер (FastAPI, Django view, консьюмер Kafka) только собирает
@dataclass(frozen=True)
class CancelOrder: # команда — входные данные сценария
order_id: str
reason: str
class CancelOrderHandler:
def __init__(self, uow, bus):
self._uow = uow
self._bus = bus
def __call__(self, cmd: CancelOrder):
with self._uow as uow:
order = uow.orders.get(OrderId(cmd.order_id))
if order is None:
raise OrderNotFound(cmd.order_id)
order.cancel(CancelReason(cmd.reason)) # правила — внутри агрегата
uow.commit()
for event in order.pull_events():
self._bus.publish(event)
Контроллер (FastAPI, Django view, консьюмер Kafka) только собирает
CancelOrder из запроса и вызывает обработчик. Один и тот же use case работает из HTTP, CLI и очереди.
34 Блок 5 из 7: Архитектура приложения — Как отделить доменную модель от ORM в Python?
Вариант 1. Classical (imperative) mapping в SQLAlchemy. Доменные классы — чистые Python-объекты, а маппинг на таблицы описан отдельно в инфраструктуре.
Вариант 2. Отдельные ORM-модели + маппинг в репозитории. Репозиторий превращает строку ORM в доменный объект и обратно. Работает с любой ORM, в том числе Django, но требует ручного кода маппинга.
Вариант 3. Прагматичный компромисс. ORM-модель и есть доменная сущность (Django
Как выбирать: чем сложнее и ценнее домен, тем выгоднее чистая модель. Для простых частей системы разделение не окупается.
# infrastructure/orm.py
mapper_registry.map_imperatively(Order, orders_table, properties={
'_lines': relationship(OrderLine),
})
Вариант 2. Отдельные ORM-модели + маппинг в репозитории. Репозиторий превращает строку ORM в доменный объект и обратно. Работает с любой ORM, в том числе Django, но требует ручного кода маппинга.
Вариант 3. Прагматичный компромисс. ORM-модель и есть доменная сущность (Django
Model с методами поведения). Проще, но домен зависит от фреймворка, а границы агрегата держатся только дисциплиной.Как выбирать: чем сложнее и ценнее домен, тем выгоднее чистая модель. Для простых частей системы разделение не окупается.
35 Блок 5 из 7: Архитектура приложения — Можно ли применять DDD в Django и какие компромиссы? 🔥
Можно, но Django построен на Active Record, а он тянет в обратную сторону: любой код может сделать
Что реально работает:
- Стратегия: Django-приложения (apps) по ограниченным контекстам, а не по техническим сущностям; никаких ForeignKey и импортов моделей между контекстами.
- Богатые модели: поведение в методах модели (
- Сервисный слой (
- Value Objects как dataclass'ы плюс кастомные поля или свойства.
- Доменные события через
Компромисс: полная изоляция домена от ORM в Django дорогая. Обычно берут стратегический DDD и часть тактики, а для core-домена с тяжёлой логикой иногда выделяют чистый Python-модуль.
Order.objects.filter(...).update(...) и обойти правила агрегата.Что реально работает:
- Стратегия: Django-приложения (apps) по ограниченным контекстам, а не по техническим сущностям; никаких ForeignKey и импортов моделей между контекстами.
- Богатые модели: поведение в методах модели (
order.cancel()), а не в views и сериализаторах.- Сервисный слой (
services.py) для use case'ов с transaction.atomic().- Value Objects как dataclass'ы плюс кастомные поля или свойства.
- Доменные события через
transaction.on_commit(), а не через post_save-сигналы.Компромисс: полная изоляция домена от ORM в Django дорогая. Обычно берут стратегический DDD и часть тактики, а для core-домена с тяжёлой логикой иногда выделяют чистый Python-модуль.
36 Блок 6 из 7: Согласованность и транзакции — Почему одна транзакция должна менять только один агрегат? 🔥
Агрегат — граница согласованности. Если в одной транзакции меняются несколько агрегатов, то:
- растёт конкуренция: два пользователя меняют разные вещи, но блокируют друг друга;
- границы агрегатов размываются: по факту получился один большой агрегат, только неявный;
- не масштабируется: когда агрегаты окажутся в разных БД или сервисах, общей транзакции уже не будет.
Как тогда: агрегат A меняется и порождает событие, обработчик в отдельной транзакции меняет агрегат B — eventual consistency.
Когда правило нарушают осознанно: если бизнес требует немедленной согласованности и агрегаты в одной БД. Но сначала стоит спросить: может, их надо объединить или пересмотреть требование? Бизнес часто спокойно живёт с задержкой в секунды.
- растёт конкуренция: два пользователя меняют разные вещи, но блокируют друг друга;
- границы агрегатов размываются: по факту получился один большой агрегат, только неявный;
- не масштабируется: когда агрегаты окажутся в разных БД или сервисах, общей транзакции уже не будет.
Как тогда: агрегат A меняется и порождает событие, обработчик в отдельной транзакции меняет агрегат B — eventual consistency.
Когда правило нарушают осознанно: если бизнес требует немедленной согласованности и агрегаты в одной БД. Но сначала стоит спросить: может, их надо объединить или пересмотреть требование? Бизнес часто спокойно живёт с задержкой в секунды.
37 Блок 6 из 7: Согласованность и транзакции — Как обеспечить согласованность между агрегатами?
Через eventual consistency — согласованность «в конечном счёте»:
1. Агрегат
2. Событие сохраняется через outbox и доставляется подписчикам.
3. Обработчик загружает агрегат
Что нужно учесть:
- идемпотентность обработчиков — событие может прийти повторно;
- порядок событий может нарушиться;
- ошибки: повторы (retry), dead letter queue, компенсирующие действия;
- UX: интерфейс должен уметь показать промежуточное состояние («заказ обрабатывается»).
Вопрос к бизнесу: «Что будет, если это обновится через 2 секунды?» Часто ответ — «ничего страшного».
1. Агрегат
Order оплачивается и записывает событие OrderPaid.2. Событие сохраняется через outbox и доставляется подписчикам.
3. Обработчик загружает агрегат
Inventory и резервирует товар в своей транзакции.Что нужно учесть:
- идемпотентность обработчиков — событие может прийти повторно;
- порядок событий может нарушиться;
- ошибки: повторы (retry), dead letter queue, компенсирующие действия;
- UX: интерфейс должен уметь показать промежуточное состояние («заказ обрабатывается»).
Вопрос к бизнесу: «Что будет, если это обновится через 2 секунды?» Часто ответ — «ничего страшного».
38 Блок 6 из 7: Согласованность и транзакции — Что такое сага и Process Manager в DDD?
Когда бизнес-процесс затрагивает несколько агрегатов или контекстов и длится во времени (оформление заказа: резерв → оплата → доставка), его координируют сагой.
- Сага — последовательность локальных транзакций, каждая из которых публикует событие для следующего шага. При ошибке выполняются компенсирующие действия (отменить резерв, вернуть деньги).
- Хореография — нет центра: каждый участник реагирует на события других. Просто, но процесс сложно проследить.
- Оркестрация / Process Manager — отдельный объект хранит состояние процесса и решает, какую команду отправить следующей, реагируя на события.
Process Manager — сам по себе агрегат со своим состоянием и репозиторием.
- Сага — последовательность локальных транзакций, каждая из которых публикует событие для следующего шага. При ошибке выполняются компенсирующие действия (отменить резерв, вернуть деньги).
- Хореография — нет центра: каждый участник реагирует на события других. Просто, но процесс сложно проследить.
- Оркестрация / Process Manager — отдельный объект хранит состояние процесса и решает, какую команду отправить следующей, реагируя на события.
OrderPlaced → ReserveStock
StockReserved → ChargePayment
PaymentFailed → ReleaseStock, CancelOrder (компенсация)
PaymentCharged → ScheduleDelivery
Process Manager — сам по себе агрегат со своим состоянием и репозиторием.
39 Блок 6 из 7: Согласованность и транзакции — Как защитить агрегат от конкурентных изменений?
Типичный подход — оптимистическая блокировка по версии агрегата.
- У агрегата есть поле
- При сохранении:
- Обновлено 0 строк → кто-то успел изменить агрегат раньше →
Почему это работает именно в DDD: агрегат — единица изменения, поэтому одной версии на корень достаточно, чтобы защитить все его инварианты. Маленькие агрегаты — меньше конфликтов.
Пессимистическая блокировка (
- У агрегата есть поле
version.- При сохранении:
UPDATE ... SET version = version + 1 WHERE id = :id AND version = :loaded_version.- Обновлено 0 строк → кто-то успел изменить агрегат раньше →
ConcurrencyError. Use case повторяют или возвращают ошибку пользователю.updated = session.execute(
update(orders)
.where(orders.c.id == order.id, orders.c.version == order.version)
.values(status=order.status, version=order.version + 1)
).rowcount
if updated == 0:
raise ConcurrencyError(order.id)
Почему это работает именно в DDD: агрегат — единица изменения, поэтому одной версии на корень достаточно, чтобы защитить все его инварианты. Маленькие агрегаты — меньше конфликтов.
Пессимистическая блокировка (
SELECT ... FOR UPDATE) уместна при высокой конкуренции за один агрегат.
40 Блок 6 из 7: Согласованность и транзакции — Как определить размер и границы агрегата?
Начинайте с маленького: корень плюс Value Objects. Добавляйте в агрегат объект, только если есть инвариант, который требует немедленной согласованности с ним.
Вопросы для проверки:
- Какие правила должны соблюдаться в момент коммита, а какие могут подождать?
- Меняют ли разные пользователи разные части объекта одновременно? Если да — это кандидаты в разные агрегаты.
- Сколько объектов загружается ради одной операции?
Пример:
Признаки слишком большого агрегата: долгая загрузка, частые конфликты версий, ленивые коллекции на тысячи элементов.
Вопросы для проверки:
- Какие правила должны соблюдаться в момент коммита, а какие могут подождать?
- Меняют ли разные пользователи разные части объекта одновременно? Если да — это кандидаты в разные агрегаты.
- Сколько объектов загружается ради одной операции?
Пример:
Forum с тысячами Post внутри — плохой агрегат: каждый новый пост блокирует весь форум. Лучше Post — отдельный агрегат со ссылкой forum_id, а правило «в закрытый форум нельзя писать» проверяется при создании поста.Признаки слишком большого агрегата: долгая загрузка, частые конфликты версий, ленивые коллекции на тысячи элементов.
41 Блок 7 из 7: Продвинутые темы и практика — Что такое CQRS и как он связан с DDD? 🔥
CQRS (Command Query Responsibility Segregation) — разделение модели на запись (команды) и модель на чтение (запросы).
- Команды идут через доменную модель: агрегаты, инварианты, события.
- Запросы идут мимо агрегатов: прямой SQL, отдельные view, денормализованные таблицы, Elasticsearch — то, что удобно экрану.
Связь с DDD: агрегаты оптимизированы под защиту инвариантов, а не под выборки. Собирать список «заказы с именами клиентов и суммами» через репозитории — медленно и неудобно. CQRS убирает этот конфликт.
Уровни:
- простой — одна БД, но запросы на чтение пишутся отдельно, в обход домена;
- продвинутый — отдельная read-БД, которая обновляется по событиям (eventual consistency).
CQRS не требует Event Sourcing, хотя их часто используют вместе.
- Команды идут через доменную модель: агрегаты, инварианты, события.
- Запросы идут мимо агрегатов: прямой SQL, отдельные view, денормализованные таблицы, Elasticsearch — то, что удобно экрану.
Связь с DDD: агрегаты оптимизированы под защиту инвариантов, а не под выборки. Собирать список «заказы с именами клиентов и суммами» через репозитории — медленно и неудобно. CQRS убирает этот конфликт.
Уровни:
- простой — одна БД, но запросы на чтение пишутся отдельно, в обход домена;
- продвинутый — отдельная read-БД, которая обновляется по событиям (eventual consistency).
CQRS не требует Event Sourcing, хотя их часто используют вместе.
42 Блок 7 из 7: Продвинутые темы и практика — Что такое Event Sourcing, его плюсы и минусы? 🔥
Event Sourcing — состояние агрегата хранится не как текущая строка в таблице, а как последовательность событий. Текущее состояние получают, проигрывая события по порядку.
Плюсы:
- полный аудит и история «из коробки»;
- можно восстановить состояние на любой момент времени;
- новые read-модели (проекции) строятся проигрыванием старых событий;
- естественно ложится на доменные события.
Минусы:
- сложность: проекции, снапшоты, версионирование схемы событий;
- запросы к текущему состоянию требуют проекций (а значит, CQRS);
- исправить ошибочные данные сложнее — события неизменяемы, нужны компенсирующие события;
- высокий порог входа для команды.
Когда уместен: финансы, учёт, где история и аудит — бизнес-требование. Для обычного CRUD — избыточен.
Account #42:
AccountOpened(owner=Аня)
MoneyDeposited(100)
MoneyWithdrawn(30)
→ баланс = 70
Плюсы:
- полный аудит и история «из коробки»;
- можно восстановить состояние на любой момент времени;
- новые read-модели (проекции) строятся проигрыванием старых событий;
- естественно ложится на доменные события.
Минусы:
- сложность: проекции, снапшоты, версионирование схемы событий;
- запросы к текущему состоянию требуют проекций (а значит, CQRS);
- исправить ошибочные данные сложнее — события неизменяемы, нужны компенсирующие события;
- высокий порог входа для команды.
Когда уместен: финансы, учёт, где история и аудит — бизнес-требование. Для обычного CRUD — избыточен.
43 Блок 7 из 7: Продвинутые темы и практика — Что такое Event Storming?
Event Storming (Альберто Брандолини) — воркшоп, на котором разработчики и эксперты домена вместе моделируют бизнес-процесс стикерами на длинной стене.
Основные стикеры:
- 🟧 доменные события — «Заказ оформлен», «Оплата получена» (выкладываются по времени);
- 🟦 команды — что вызвало событие;
- 🟨 акторы и агрегаты — кто инициирует и что изменяется;
- 🟪 политики — «когда X, тогда Y»;
- 🟥 горячие точки — вопросы, противоречия, проблемы.
Результат: общее понимание процесса, единый язык, кандидаты в агрегаты и границы ограниченных контекстов. Быстрый способ начать DDD на новом проекте.
Основные стикеры:
- 🟧 доменные события — «Заказ оформлен», «Оплата получена» (выкладываются по времени);
- 🟦 команды — что вызвало событие;
- 🟨 акторы и агрегаты — кто инициирует и что изменяется;
- 🟪 политики — «когда X, тогда Y»;
- 🟥 горячие точки — вопросы, противоречия, проблемы.
Результат: общее понимание процесса, единый язык, кандидаты в агрегаты и границы ограниченных контекстов. Быстрый способ начать DDD на новом проекте.
44 Блок 7 из 7: Продвинутые темы и практика — Что такое паттерн Спецификация (Specification)?
Specification — объект, который инкапсулирует бизнес-правило отбора в виде предиката
Применение:
- проверка: можно ли выполнить действие;
- выборка: репозиторий принимает спецификацию и превращает её в запрос;
- создание: «собрать объект, удовлетворяющий условию».
Плюс: правило в одном месте с понятным именем, а не размазано по
is_satisfied_by(candidate). Правила получают имена из единого языка и комбинируются через and/or/not.class OverdueInvoice:
def __init__(self, today):
self.today = today
def is_satisfied_by(self, invoice):
return not invoice.is_paid and invoice.due_date < self.today
class VipCustomer:
def is_satisfied_by(self, customer):
return customer.total_spent >= Money(100_000, 'RUB')
Применение:
- проверка: можно ли выполнить действие;
- выборка: репозиторий принимает спецификацию и превращает её в запрос;
- создание: «собрать объект, удовлетворяющий условию».
Плюс: правило в одном месте с понятным именем, а не размазано по
if и фильтрам QuerySet.
45 Блок 7 из 7: Продвинутые темы и практика — Как тестировать доменную модель?
Доменная модель без зависимостей тестируется быстрыми юнит-тестами без БД и моков:
Пирамида:
- домен — много юнит-тестов на инварианты и события;
- use case'ы — тесты с
- адаптеры — немного интеграционных тестов с реальной БД (репозитории, маппинг);
- e2e — несколько сценариев через API.
Тесты пишутся на едином языке: «нельзя оплатить отменённый заказ» — это одновременно спецификация бизнес-правила.
def test_cannot_pay_cancelled_order():
order = make_order(total=Money(500, 'RUB'))
order.cancel(CancelReason.CUSTOMER_REQUEST)
with pytest.raises(OrderCancelled):
order.pay(Payment(Money(500, 'RUB')))
def test_paying_records_event():
order = make_order(total=Money(500, 'RUB'))
order.pay(Payment(Money(500, 'RUB')))
assert order.pull_events() == [OrderPaid(order.id, Money(500, 'RUB'))]
Пирамида:
- домен — много юнит-тестов на инварианты и события;
- use case'ы — тесты с
FakeUnitOfWork и in-memory репозиториями;- адаптеры — немного интеграционных тестов с реальной БД (репозитории, маппинг);
- e2e — несколько сценариев через API.
Тесты пишутся на едином языке: «нельзя оплатить отменённый заказ» — это одновременно спецификация бизнес-правила.
46 Блок 7 из 7: Продвинутые темы и практика — Какие типичные ошибки при внедрении DDD? 🔥
- Только тактика без стратегии: добавили репозитории и агрегаты, но без единого языка и границ контекстов — получилась церемония без пользы.
- DDD для CRUD: слои, UoW и события там, где достаточно формы и таблицы.
- Анемичная модель под видом DDD: «сущности» — dataclass'ы с полями, логика в сервисах.
- Гигантские агрегаты и ссылки объектами на чужие агрегаты.
- Одна общая модель на всю компанию: единый
- Общая БД между контекстами, интеграция через чужие таблицы.
- Нарезка микросервисов не по контекстам → распределённый монолит.
- Нет доступа к экспертам: модель придумывают разработчики, язык расходится с бизнесом.
- Доменные события наружу как есть вместо стабильных интеграционных контрактов.
- DDD для CRUD: слои, UoW и события там, где достаточно формы и таблицы.
- Анемичная модель под видом DDD: «сущности» — dataclass'ы с полями, логика в сервисах.
- Гигантские агрегаты и ссылки объектами на чужие агрегаты.
- Одна общая модель на всю компанию: единый
User или Product со 100 полями для всех.- Общая БД между контекстами, интеграция через чужие таблицы.
- Нарезка микросервисов не по контекстам → распределённый монолит.
- Нет доступа к экспертам: модель придумывают разработчики, язык расходится с бизнесом.
- Доменные события наружу как есть вместо стабильных интеграционных контрактов.
47 Блок 7 из 7: Продвинутые темы и практика — Как спроектировать по DDD оформление заказа в интернет-магазине? 🔥
1. Контексты: Каталог, Корзина, Заказы, Оплата (generic — внешний провайдер через ACL), Склад, Доставка.
2. Агрегат
- корень
- внутри
- Value Objects:
- инварианты: нельзя менять состав после оплаты, оплачивается ровно сумма заказа, отменить можно только до отгрузки.
3. Процесс (сага):
4. Технически: одна транзакция — один агрегат, события через outbox, идемпотентные обработчики, оптимистическая блокировка по
Что хотят услышать на интервью: границы контекстов, состав агрегата и его инварианты, почему цена копируется в заказ, как достигается согласованность между контекстами.
2. Агрегат
Order в контексте «Заказы»:- корень
Order (id, customer_id, статус, version);- внутри
OrderLine — product_id, количество и цена на момент заказа (Value Object Money), а не ссылка на товар каталога;- Value Objects:
Money, Address;- инварианты: нельзя менять состав после оплаты, оплачивается ровно сумма заказа, отменить можно только до отгрузки.
3. Процесс (сага):
PlaceOrder → OrderPlaced
→ Склад: резерв → StockReserved / StockShortage
→ Оплата: списание → PaymentCaptured / PaymentFailed
→ Заказ: mark_paid → OrderPaid → Доставка: создать отправление
Ошибка на любом шаге → компенсация: снять резерв, отменить заказ.
4. Технически: одна транзакция — один агрегат, события через outbox, идемпотентные обработчики, оптимистическая блокировка по
version, список заказов в кабинете — отдельная read-модель (CQRS).Что хотят услышать на интервью: границы контекстов, состав агрегата и его инварианты, почему цена копируется в заказ, как достигается согласованность между контекстами.
Ничего не нашлось — попробуйте другой запрос.
Хотите не просто прочитать, а запомнить?
Бот вернёт вам эти вопросы ровно тогда, когда вы начнёте их забывать.