Logo
Overview

DDD простыми словами: bounded contexts, aggregates и ubiquitous language

April 20, 2026
11 min read

DDD — это не про код. Это про то, как вы думаете о системе до того, как напишете первую строку.

Если вы когда-нибудь гуглили «что такое DDD» и через 10 минут чтения чувствовали себя глупее, чем до — вы не одиноки. Большинство статей о Domain-Driven Design написаны людьми, которые уже всё поняли, и для людей, которые уже почти всё поняли. Остальным — удачи.

Эта статья для тех, кто слышал слова «bounded context», «aggregate» и «ubiquitous language», кивал на созвоне, а потом тихо искал в Google, что это значит. Мы разберём три ключевых концепции DDD так, чтобы после прочтения вы могли не просто кивать, а аргументированно спорить на архитектурных ревью.

Если вы хотите освежить общее понимание подхода, загляните в обзорную статью о DDD — там мы разбирали, кому и зачем это нужно. Здесь пойдём глубже.

Ubiquitous Language: единый язык, без которого всё разваливается

Давайте начнём с концепции, которую чаще всего недооценивают. Ubiquitous Language (единый язык) — это не глоссарий, который написали в Confluence и забыли. Это соглашение между бизнесом и разработкой о том, как называть вещи — и в разговоре, и в коде, и в документации.

Ubiquitous Language — это когда бизнес-аналитик говорит «полис», тимлид говорит «полис», разработчик пишет в коде Policy, и тестировщик проверяет именно Policy. Не Document, не Contract, не InsuranceItem.

Почему это так важно

Представьте страховую компанию. Менеджер продукта говорит «клиент». Разработчик в коде пишет User. Аналитик в требованиях пишет «застрахованное лицо». Тестировщик в тест-кейсах называет это «пользователь». Все говорят об одном и том же — но каждый по-своему.

Через полгода появляется новая фича: «клиент может быть юридическим лицом». И начинается хаос. В одном месте User расширяют, в другом создают CompanyUser, в третьем — LegalEntity. А менеджер продукта всё ещё думает, что «клиент» — это просто клиент.

С единым языком эта проблема решается на старте. Команда договаривается: в контексте оформления полиса «клиент» = Policyholder (страхователь). В контексте выплат — Claimant (заявитель). Это разные роли, и они имеют право быть разными сущностями.

Как внедрить Ubiquitous Language на практике

Это не разовое мероприятие, а непрерывный процесс:

Что делатьКак именно
Создать глоссарий доменаMarkdown-файл в репозитории (не Confluence — он умрёт раньше проекта). Формат: термин → определение → пример в коде
Называть классы и методы на языке бизнесаНе processData(), а calculatePremium(). Не handleEvent(), а approveClaimPayment()
Проводить Event StormingВоркшоп, где бизнес и разработка вместе выписывают события домена на стикерах. Лучший способ синхронизировать язык
Ревьюить код на соответствие языкуЕсли в коде User, а в домене «Страхователь» — это баг, даже если тесты зелёные

Bounded Context: границы, которые спасают от хаоса

Bounded Context (ограниченный контекст) — это, пожалуй, самая важная и самая неправильно понимаемая концепция DDD. Многие думают, что bounded context = микросервис. Это не так (хотя связь есть, и мы до неё дойдём).

Bounded Context — это граница, внутри которой определённая модель домена имеет единственное, непротиворечивое значение. За пределами этой границы та же модель может значить что-то совсем другое.

Аналогия: одно слово — разные значения

Слово «счёт» в банке означает одно, в ресторане — совсем другое, а в футболе — третье. Если вы попросите «выставить счёт» — результат зависит от контекста. Bounded Context — это и есть этот контекст. Он определяет, что именно означает каждый термин внутри себя.

В интернет-магазине «товар» в контексте каталога — это карточка с описанием, фотографиями и характеристиками. «Товар» в контексте склада — это физическая единица с весом, габаритами и адресом ячейки хранения. «Товар» в контексте доставки — это строчка в накладной с идентификатором и количеством. Три разных bounded context — три разных модели для одного и того же физического объекта.

100%
graph TD
  subgraph CTX_CATALOG["Контекст: Каталог"]
      direction TB
      P1["Товар<br/><i>Product</i>"]
      P1_DESC["название, описание,<br/>фото, характеристики,<br/>цена, рейтинг"]
      P1 --- P1_DESC
  end

  subgraph CTX_WAREHOUSE["Контекст: Склад"]
      direction TB
      P2["Товар<br/><i>StockItem</i>"]
      P2_DESC["SKU, вес, габариты,<br/>ячейка хранения,<br/>остаток, партия"]
      P2 --- P2_DESC
  end

  subgraph CTX_DELIVERY["Контекст: Доставка"]
      direction TB
      P3["Товар<br/><i>ShipmentLine</i>"]
      P3_DESC["идентификатор,<br/>количество, вес,<br/>статус доставки"]
      P3 --- P3_DESC
  end

  subgraph CTX_BILLING["Контекст: Биллинг"]
      direction TB
      P4["Товар<br/><i>InvoiceItem</i>"]
      P4_DESC["наименование,<br/>цена, количество,<br/>НДС, скидка"]
      P4 --- P4_DESC
  end

  P1 -.->|"ProductId"| P2
  P2 -.->|"SKU"| P3
  P1 -.->|"ProductId"| P4

  style P1 fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style P2 fill:#50c878,stroke:#3a9a5c,color:#fff
  style P3 fill:#f0a500,stroke:#c88400,color:#fff
  style P4 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style P1_DESC fill:#fff,stroke:#2c5f8a,color:#333
  style P2_DESC fill:#fff,stroke:#3a9a5c,color:#333
  style P3_DESC fill:#fff,stroke:#c88400,color:#333
  style P4_DESC fill:#fff,stroke:#5a4db2,color:#333

Диаграмма показывает, как один и тот же «товар» превращается в четыре разные модели в зависимости от контекста. Обратите внимание на пунктирные линии — это связи между контекстами. Они идут через идентификаторы (ProductId, SKU), а не через общие объекты. Каждый контекст владеет своей моделью и не зависит от внутренностей другого.

Как определить границы контекстов

Вот несколько сигналов, что пора провести границу:

  • Одно слово означает разное. Если «заказ» для отдела продаж и для склада — это разные вещи с разными атрибутами, у вас два контекста.
  • Разные команды работают с разными частями системы. Команда каталога и команда логистики не должны менять одни и те же таблицы в базе данных.
  • Разная частота изменений. Каталог обновляется каждый день, а биллинг-модуль не трогают месяцами. Значит, им не надо жить в одном коде.
  • Разные правила согласованности. Каталог может быть eventually consistent (обновил — отобразится через 5 секунд), а биллинг требует strict consistency (списание должно быть атомарным).

Bounded Context и модульный монолит

Bounded context — это логическая граница. Она не требует отдельного сервиса или отдельной базы данных. Вы можете реализовать несколько bounded contexts внутри модульного монолита — и это будет абсолютно правильный подход. Модуль = bounded context, только всё в одном деплое.

Переход от модульного монолита к микросервисам — это, по сути, вынесение bounded contexts в отдельные процессы. Если границы проведены правильно, эта миграция проходит относительно безболезненно (относительно — ключевое слово).

Aggregate: единица консистентности, которая не даёт данным развалиться

Aggregate (агрегат) — это кластер связанных объектов, которые мы рассматриваем как единое целое с точки зрения изменения данных. У агрегата есть корневой объект (Aggregate Root), через который происходит всё взаимодействие с внешним миром.

Агрегат — это коробка с инструкцией. Вы можете взять коробку целиком, можете положить обратно, но не можете вытащить одну деталь и изменить её в обход коробки. Всё проходит через корень.

Зачем нужен агрегат

Представьте интернет-магазин. У вас есть заказ (Order), в нём — позиции (OrderLine), у каждой позиции — ссылка на товар и количество. Если два запроса одновременно пытаются изменить один и тот же заказ — например, клиент добавляет товар, а менеджер меняет скидку — то без агрегата вы рискуете получить несогласованные данные. Агрегат гарантирует, что все изменения внутри него проходят атомарно и согласованно.

100%
graph TD
  subgraph AGG_ORDER["Агрегат: Заказ"]
      direction TB
      ROOT["Order<br/><b>Aggregate Root</b><br/><i>orderId, status, total</i>"]
      LINE1["OrderLine #1<br/><i>productId, qty, price</i>"]
      LINE2["OrderLine #2<br/><i>productId, qty, price</i>"]
      LINE3["OrderLine #3<br/><i>productId, qty, price</i>"]
      ADDR["DeliveryAddress<br/><i>city, street, zip</i>"]
      DISC["Discount<br/><i>type, percent, code</i>"]

      ROOT --> LINE1
      ROOT --> LINE2
      ROOT --> LINE3
      ROOT --> ADDR
      ROOT --> DISC
  end

  EXT_CLIENT["Клиент<br/><i>другой агрегат</i>"]
  EXT_PRODUCT["Товар<br/><i>другой контекст</i>"]
  EXT_PAYMENT["Платёж<br/><i>другой агрегат</i>"]

  EXT_CLIENT -->|"customerId"| ROOT
  LINE1 -.->|"productId<br/>(только ссылка)"| EXT_PRODUCT
  ROOT -.->|"orderId"| EXT_PAYMENT

  style ROOT fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style LINE1 fill:#50c878,stroke:#3a9a5c,color:#fff
  style LINE2 fill:#50c878,stroke:#3a9a5c,color:#fff
  style LINE3 fill:#50c878,stroke:#3a9a5c,color:#fff
  style ADDR fill:#7b68ee,stroke:#5a4db2,color:#fff
  style DISC fill:#f0a500,stroke:#c88400,color:#fff
  style EXT_CLIENT fill:#e0e0e0,stroke:#999,color:#333
  style EXT_PRODUCT fill:#e0e0e0,stroke:#999,color:#333
  style EXT_PAYMENT fill:#e0e0e0,stroke:#999,color:#333

На диаграмме — агрегат «Заказ». Корень агрегата (Order) — единственная точка входа. Позиции заказа, адрес доставки и скидка живут внутри агрегата и не могут быть изменены извне напрямую. Серые блоки — внешние агрегаты и контексты. Обратите внимание: связь с ними идёт через идентификаторы, а не через прямые ссылки на объекты.

Правила проектирования агрегатов

Вот основные принципы, которые помогут не наступить на грабли:

1. Маленький агрегат — лучше большого.

Начинающие любят пихать в один агрегат всё подряд: заказ + клиент + товары + отзывы + историю доставки. Получается монстр, который блокирует половину базы данных при каждом изменении. Правило: агрегат должен быть минимально достаточным для обеспечения одного бизнес-инварианта.

2. Ссылки по идентификатору, а не по объекту.

Агрегат «Заказ» хранит customerId, а не объект Customer целиком. Это позволяет загружать и сохранять агрегат независимо. Если вам нужен полный объект клиента — обратитесь в соответствующий сервис.

3. Один агрегат — одна транзакция.

Изменения в одном агрегате сохраняются в одной транзакции. Если нужно изменить два агрегата — это две транзакции, и между ними eventual consistency. Да, это менее «удобно», чем один гигантский BEGIN...COMMIT, но зато масштабируется.

4. Бизнес-правила живут внутри.

Агрегат сам проверяет свои инварианты. «Заказ не может иметь отрицательную сумму», «нельзя добавить больше 50 позиций», «скидка не может превышать 30%» — всё это логика агрегата, а не внешнего сервиса.

Агрегат vs Entity vs Value Object

Чтобы окончательно разложить всё по полочкам:

КонцепцияЧто этоПримерИдентичность
Entity (сущность)Объект с уникальной идентичностью, важен «кто это»Заказ #12345, Клиент ИвановОпределяется по id
Value Object (объект-значение)Объект без идентичности, важно «что в нём»Адрес (город, улица, дом), Деньги (100 руб.)Определяется по содержимому
Aggregate (агрегат)Кластер сущностей и объектов-значений с единым корнемЗаказ + позиции + адрес доставкиОпределяется по корню

Адрес доставки — value object. Если два заказа отправляются по одному адресу, это два одинаковых value object, а не ссылка на один и тот же объект. Замените улицу — и вы создали новый value object, а не изменили старый.

Как три концепции работают вместе: пример из e-commerce

Давайте соберём всё в одну картину. Представьте, что мы проектируем систему для маркетплейса.

100%
graph LR
  subgraph UL["Ubiquitous Language"]
      direction TB
      GLOSSARY["Глоссарий домена<br/><i>Заказ, Товар, Продавец,<br/>Покупатель, Возврат</i>"]
  end

  subgraph BC_ORDERS["Bounded Context:<br/>Управление заказами"]
      direction TB
      AGG_ORD["Агрегат: Заказ<br/><i>Order + OrderLine<br/>+ DeliveryAddress</i>"]
      AGG_CART["Агрегат: Корзина<br/><i>Cart + CartItem</i>"]
  end

  subgraph BC_CATALOG["Bounded Context:<br/>Каталог"]
      direction TB
      AGG_PROD["Агрегат: Товар<br/><i>Product + Variant<br/>+ Price</i>"]
      AGG_CAT["Агрегат: Категория<br/><i>Category + Attributes</i>"]
  end

  subgraph BC_PAYMENTS["Bounded Context:<br/>Платежи"]
      direction TB
      AGG_PAY["Агрегат: Платёж<br/><i>Payment + Transaction<br/>+ Refund</i>"]
  end

  subgraph BC_DELIVERY["Bounded Context:<br/>Доставка"]
      direction TB
      AGG_SHIP["Агрегат: Отправление<br/><i>Shipment + TrackingEvent</i>"]
  end

  UL -.->|"единый язык<br/>во всех контекстах"| BC_ORDERS
  UL -.->|"единый язык"| BC_CATALOG
  UL -.->|"единый язык"| BC_PAYMENTS
  UL -.->|"единый язык"| BC_DELIVERY

  BC_ORDERS -->|"OrderPlaced<br/>событие"| BC_PAYMENTS
  BC_ORDERS -->|"OrderConfirmed<br/>событие"| BC_DELIVERY
  BC_CATALOG -->|"productId"| BC_ORDERS

  style GLOSSARY fill:#f0a500,stroke:#c88400,color:#fff
  style AGG_ORD fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style AGG_CART fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style AGG_PROD fill:#50c878,stroke:#3a9a5c,color:#fff
  style AGG_CAT fill:#50c878,stroke:#3a9a5c,color:#fff
  style AGG_PAY fill:#7b68ee,stroke:#5a4db2,color:#fff
  style AGG_SHIP fill:#f0a500,stroke:#c88400,color:#fff

Диаграмма показывает полную картину: ubiquitous language пронизывает все контексты (пунктирные линии), bounded contexts определяют границы (рамки), а агрегаты — единицы консистентности внутри каждого контекста (блоки). Связь между контекстами — через идентификаторы и доменные события (OrderPlaced, OrderConfirmed), а не через прямые вызовы.

Что это даёт на практике

  • Команда каталога может менять структуру карточки товара, и это не ломает заказы. Потому что контекст заказов знает о товаре только productId.
  • Команда платежей может добавить новый платёжный метод, и это не требует изменений в коде заказов. Потому что связь — через событие OrderPlaced.
  • Новый разработчик читает глоссарий и понимает, что Claimant в контексте возвратов — это не тот же Buyer, хотя физически это один человек.

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

Ошибка 1: «Давайте сразу весь проект на DDD»

DDD — это не всё или ничего. Эрик Эванс (автор оригинальной книги) сам говорит: применяйте DDD к Core Domain — к самой важной и сложной части системы. CRUD-админка для управления справочниками не нуждается в агрегатах и bounded contexts. Это нормально.

Ошибка 2: Bounded Context = микросервис

Bounded context — это логическая граница. Микросервис — это физическая граница (отдельный процесс, деплой, база). Один bounded context может жить как модуль в модульном монолите. А может стать микросервисом. Или даже несколькими микросервисами. Не путайте карту и территорию.

Ошибка 3: Гигантские агрегаты

«А давайте Order будет содержать Customer, который содержит Address, который содержит City, который содержит Country…» — и вот у вас агрегат, который загружает полбазы данных. Держите агрегаты маленькими. Если сомневаетесь — разделяйте.

Ошибка 4: Забыть про Ubiquitous Language

Многие начинают с bounded contexts и агрегатов (потому что это звучит «технично»), но пропускают единый язык. В итоге в коде Order, в документации «заявка», на созвоне «тикет», а в базе — request. Четыре названия для одной сущности. DDD без ubiquitous language — это как навигатор без карты: механизм есть, а маршрута нет.

Когда DDD не нужен (и это нормально)

Это не серебряная пуля. DDD избыточен, если:

  • Домен простой. Приложение для учёта задач, блог, лендинг — здесь CRUD достаточно. Не нужно моделировать bounded contexts для формы обратной связи.
  • Команда из 1–2 человек. DDD требует коммуникации между бизнесом и разработкой. Если вы сам себе аналитик, разработчик и тестировщик — ubiquitous language формируется автоматически (в вашей голове).
  • Прототип или MVP. Когда главное — проверить гипотезу, а не построить идеальную архитектуру. «Работает — не трогай» на этом этапе — вполне разумная стратегия.

Но если ваш проект растёт, домен усложняется, а команда расширяется — именно в этот момент отсутствие DDD начинает больно кусать. И чем позже вы начнёте, тем дороже обойдётся рефакторинг.

Что почитать дальше

Если тема зацепила, вот осмысленный порядок погружения:

  1. Эрик Эванс — «Domain-Driven Design: Tackling Complexity in the Heart of Software» — та самая «синяя книга». Тяжёлая, но фундаментальная.
  2. Вон Вернон — «Implementing Domain-Driven Design» — «красная книга». Более практичная, с примерами на Java.
  3. Вон Вернон — «Domain-Driven Design Distilled» — если 600 страниц страшно. Квинтэссенция за 150 страниц.
  4. Event Storming (Альберто Брандолини) — метод обнаружения bounded contexts через совместные воркшопы. Начните с бесплатных материалов на eventstorming.com.

Заключение

DDD — это три простых идеи в непростой обёртке. Единый язык — чтобы все говорили одинаково. Bounded contexts — чтобы каждая часть системы знала свои границы. Агрегаты — чтобы данные не расползались как тесто без формы.

Ни одна из этих идей не требует специального фреймворка, дорогого инструмента или трёхмесячного обучения. Они требуют только одного — привычки думать о системе в терминах бизнес-домена, а не в терминах таблиц базы данных.

Начните с малого: договоритесь с командой о десяти ключевых терминах вашего домена. Запишите их. Убедитесь, что код использует те же названия. Поздравляю — вы уже практикуете DDD.

P.S. Когда на следующем собеседовании вас спросят «что такое DDD?», не отвечайте определением из Википедии. Просто скажите: «Это когда код назван так же, как бизнес-процессы, бизнес-процессы разделены на контексты, а данные не меняются в обход правил». И наблюдайте, как интервьюер одобрительно кивает — потому что он тоже только что это загуглил.