DDD простыми словами: что такое Domain-Driven Design и как его применять

DDD, или Domain-Driven Design, строит код вокруг модели бизнеса: единый язык, ограниченные контексты, сущности, объекты-значения и агрегаты.

Разбираем термины на примере онлайн-школы, переписываем анемичную модель в агрегат на JavaScript с выводом из терминала и объясняем, когда подход избыточен.

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 2906 статей автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3560 экспертных мнений Подписаться на Телеграм-канал
Обложка: DDD простыми словами: что такое Domain-Driven Design и как его применять

DDD (Domain-Driven Design, предметно-ориентированное проектирование) — подход к архитектуре, при котором код строят вокруг модели бизнеса, а не вокруг таблиц базы данных и фреймворка. Разработчики и эксперты предметной области договариваются об общем языке, делят систему на ограниченные контексты и прячут бизнес-правила внутрь объектов, которые сами следят за своей целостностью. Это один из способов выстроить архитектуру приложения там, где логики много и она часто меняется.

Ниже разберём термины на одном сквозном примере, перепишем анемичную модель в агрегат на JavaScript и покажем, когда DDD не нужен вовсе.

CheckroiCheckroiПодборка курсов по Backend-разработке129 курсов • 26 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Что такое DDD простыми словами

Рой-архитектор рисует модель предметной области на доске

Термин взят из книги Эрика Эванса 2003 года «Domain-Driven Design: Tackling Complexity in the Heart of Software». Мартин Фаулер считает, что ей место на полке любого серьёзного разработчика, и признаёт, что у книги репутация тяжёлой для чтения.

Главная мысль Эванса: сложность больших систем сидит в логике бизнеса, технологии тут вторичны. Если программист понимает слово «заказ» иначе, чем менеджер, баг появится раньше первого коммита.

Поэтому в основе DDD три идеи:

  • Единый язык (ubiquitous language): термины бизнеса без перевода попадают в названия классов, методов и событий.
  • Ограниченный контекст (bounded context): у каждого слова есть граница, внутри которой его смысл однозначен.
  • Модель с поведением: правила живут в объектах предметной области, контроллеры только передают им команды.

Вот как единый язык выглядит на практике в онлайн-школе:

Как говорит бизнес Как назвали бы без DDD Как в коде с DDD
«Записать студента на поток» updateUserGroup(userId, groupId) cohort.enroll(studentId)
«На потоке закончились места» if (count >= limit) return 409 ошибка «Мест нет» внутри Cohort
«Студент записался» строка в таблице логов событие StudentEnrolled

Словарь обычно собирает системный аналитик вместе с командой, но поддерживают его все. Если в разговоре появилось новое слово, оно появляется и в коде.

Стратегическое проектирование: ограниченные контексты

Три ограниченных контекста в виде отдельных островков с мостами

Стратегическая часть отвечает на вопрос, как разрезать большую систему на модели поменьше. По Фаулеру, именно её Эванс описал убедительнее всех до него.

Возьмём то же слово «студент». В трёх отделах школы это три разных объекта:

Контекст Что для него студент Важные поля и действия
Продажи лид, который может купить телефон, источник заявки, промокод
Обучение участник потока сданные задания, куратор, прогресс
Бухгалтерия плательщик по договору реквизиты, график платежей, возвраты

Одна общая таблица users с сотней колонок на все три случая и есть то, что DDD называет большим комком грязи (Big Ball of Mud). Каждый контекст получает свою модель, а связи между ними описывают на карте контекстов.

В справочнике DDD Reference Эванс перечисляет типовые отношения между контекстами:

  • Общее ядро (Shared Kernel): две команды делят небольшой кусок модели и меняют его только вместе.
  • Заказчик и поставщик (Customer/Supplier): нижний по течению контекст ставит задачи верхнему.
  • Конформист (Conformist): берём чужую модель как есть, потому что повлиять на неё не можем.
  • Предохранительный слой (Anticorruption Layer): переводчик, который не пускает чужие термины внутрь нашей модели.
  • Раздельные пути (Separate Ways): контексты вообще не интегрируются.

На практике один контекст часто становится отдельным сервисом. Это удобная граница для микросервисной архитектуры, но не обязательная: модульный монолит с чёткими границами тоже честный DDD.

Канал основателя Checkroi Вани Буявца3 700 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Тактические паттерны: из чего строится модель

Тактика описывает строительные блоки внутри одного контекста. Если вы знакомы с классами и объектами, половина терминов покажется знакомой, просто у каждого блока здесь своя роль.

Паттерн Что это Пример в онлайн-школе
Сущность (Entity) объект с идентичностью, которая не меняется со временем студент, поток
Объект-значение (Value Object) объект без идентичности, сравнивается по значению и не меняется цена, email, период обучения
Агрегат (Aggregate) группа объектов с одним корнем, правила проверяются через корень поток со списком записавшихся
Доменное событие (Domain Event) факт, который уже случился и важен бизнесу «студент записан», «оплата прошла»
Репозиторий (Repository) коллекция агрегатов, за которой спрятана база данных хранилище потоков
Сервис (Service) операция, которая не принадлежит ни одному объекту расчёт скидки по двум договорам
Фабрика (Factory) сборка сложного агрегата в корректном состоянии создание потока по шаблону курса

В оригинальной книге доменных событий как отдельного паттерна не было. В справочнике 2015 года Эванс добавил их с пометкой «новый термин», вместе с партнёрством контекстов и «большим комком грязи».

Самое важное правило тактики: агрегат меняют только через его корень. Внешний код не лезет в список студентов напрямую, он просит поток записать студента. Отсюда растут и принципы SOLID в DDD-проектах: у каждого объекта одна понятная ответственность.

CheckroiCheckroiПодборка курсов по ООП171 курс • 33 школыСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Пример DDD на JavaScript: от анемичной модели к агрегату

Код ниже запускали на Node.js 25.6.1, вывод вставлен из терминала. Достаточно базового JavaScript и понимания классов.

Шаг 1: анемичная модель и её проблема

Так часто пишут без DDD: класс хранит данные, а правило «не больше мест, чем вмещает поток» живёт в отдельном сервисе.

// Анемичная модель: данные отдельно, правила в сервисе
class Cohort {
  constructor(id, capacity) {
    this.id = id;
    this.capacity = capacity;
    this.students = [];
  }
}

class EnrollmentService {
  enroll(cohort, studentId) {
    if (cohort.students.length >= cohort.capacity) {
      throw new Error('Мест нет');
    }
    cohort.students.push(studentId);
  }
}

const cohort = new Cohort('python-oct', 2);
new EnrollmentService().enroll(cohort, 'anna');
new EnrollmentService().enroll(cohort, 'boris');
cohort.students.push('vera'); // правило обошли в одну строку
console.log(cohort.students.length, 'из', cohort.capacity);
3 из 2

Третий студент попал на поток с двумя местами. Фаулер ещё в 2003 году назвал такой подход анемичной моделью предметной области: объекты превращаются в наборы геттеров и сеттеров, а логика уходит в процедурные сервисы.

Шаг 2: объект-значение и агрегат

Переносим правило внутрь потока и закрываем список студентов приватным полем. Цену делаем объектом-значением: она неизменяема и сравнивается по сумме и валюте.

// Объект-значение: сравнивается по значению, не меняется
class Money {
  constructor(amount, currency) {
    if (!Number.isInteger(amount) || amount < 0) {
      throw new Error('Сумма — целое число копеек от нуля');
    }
    this.amount = amount;
    this.currency = currency;
    Object.freeze(this);
  }
  equals(other) {
    return this.amount === other.amount && this.currency === other.currency;
  }
}

// Агрегат: поток курса сам охраняет свои правила
class Cohort {
  #students = new Set();
  #events = [];

  constructor(id, capacity, price) {
    this.id = id;
    this.capacity = capacity;
    this.price = price;
  }

  enroll(studentId) {
    if (this.#students.has(studentId)) {
      throw new Error(`${studentId} уже записан`);
    }
    if (this.#students.size >= this.capacity) {
      throw new Error('Мест нет');
    }
    this.#students.add(studentId);
    this.#events.push({ type: 'StudentEnrolled', cohortId: this.id, studentId });
  }

  get seatsLeft() {
    return this.capacity - this.#students.size;
  }

  pullEvents() {
    const events = this.#events;
    this.#events = [];
    return events;
  }
}

const price = new Money(4990000, 'RUB');
console.log(price.equals(new Money(4990000, 'RUB'))); // true

const cohort = new Cohort('python-oct', 2, price);
cohort.enroll('anna');
cohort.enroll('boris');
console.log('Свободных мест:', cohort.seatsLeft);

try {
  cohort.enroll('vera');
} catch (e) {
  console.log('Ошибка:', e.message);
}

console.log(cohort.students); // undefined: снаружи список не достать
console.log(cohort.pullEvents());
true
Свободных мест: 0
Ошибка: Мест нет
undefined
[
  {
    type: 'StudentEnrolled',
    cohortId: 'python-oct',
    studentId: 'anna'
  },
  {
    type: 'StudentEnrolled',
    cohortId: 'python-oct',
    studentId: 'boris'
  }
]

Теперь обойти правило нельзя: поле #students приватное, и любой вызов идёт через enroll(). Заодно агрегат копит доменные события. Их потом отправит инфраструктура, например письмо куратору или запись в аналитику.

Деньги храним в копейках целым числом. Дробные рубли во float дают ошибки округления, поэтому 4990000 здесь означает 49 900 ₽.

Шаг 3: репозиторий и сервис приложения

Агрегат ничего не знает о базе данных. Сохранением занимается репозиторий, а сценарий «записать студента» собирает сервис приложения: загрузил агрегат, вызвал его метод, сохранил.

// Репозиторий: домен знает только «найти» и «сохранить»
class InMemoryCohortRepository {
  #items = new Map();
  async findById(id) { return this.#items.get(id) ?? null; }
  async save(cohort) { this.#items.set(cohort.id, cohort); }
}

// Сервис приложения: загрузил агрегат, вызвал его метод, сохранил
async function enrollStudent(repo, cohortId, studentId) {
  const cohort = await repo.findById(cohortId);
  if (!cohort) throw new Error('Поток не найден');
  cohort.enroll(studentId);
  await repo.save(cohort);
  return cohort.pullEvents();
}

const repo = new InMemoryCohortRepository();
await repo.save(new Cohort('sql-nov', 30, new Money(2990000, 'RUB')));
console.log(await enrollStudent(repo, 'sql-nov', 'gleb'));
[ { type: 'StudentEnrolled', cohortId: 'sql-nov', studentId: 'gleb' } ]

Этот фрагмент запускали в одном файле с классами Money и Cohort. Файл назвали .mjs, потому что await на верхнем уровне работает только в ES-модулях. В реальном проекте на Node.js класс InMemoryCohortRepository заменяют на реализацию поверх PostgreSQL или другой базы, а код агрегата при этом не меняется.

CheckroiCheckroiПодборка курсов по Javascript209 курсов • 37 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Как внедрить DDD в проект: пошагово

Начинать с паттернов и папок domain/ бесполезно. Порядок обычно такой:

  1. Соберите людей, которые знают бизнес: руководителя направления, операторов, бизнес-аналитика. Без эксперта предметной области получится DDD только по названиям классов.
  2. Выпишите события процесса в прошедшем времени: «заявка оставлена», «договор подписан», «студент записан». На стикерах или в онлайн-доске.
  3. Найдите места, где слова меняют смысл. Там и проходят границы контекстов.
  4. Нарисуйте карту контекстов и подпишите, кто от кого зависит.
  5. Выберите главную предметную область (core domain), ради которой бизнес существует. Тактические паттерны тратим на неё, а типовые вещи вроде авторизации берём готовыми.
  6. Заведите словарь единого языка и правьте его при каждом новом термине.
  7. Рефакторьте по одному агрегату. Переписывать всю систему разом рискованно, удобнее переносить правила внутрь объектов постепенно.

Карту контекстов и схемы агрегатов удобно рисовать в нотации UML или C4. Такие схемы каждый день обсуждает с бизнесом и командой архитектор ПО.

Когда DDD не нужен и частые ошибки

Довольный Рой отмечает галочки в чек-листе рядом с простой схемой приложения

Фаулер подчёркивает, что подход особенно подходит для сложных предметных областей с запутанной логикой. Обратная сторона: в простом проекте он добавит слоёв и замедлит команду.

DDD избыточен, если приложение почти целиком CRUD (создать, прочитать, обновить, удалить), бизнес-правил единицы, а эксперта предметной области в команде нет.

Ошибки, которые встречаются у новичков чаще всего:

  • Паттерны без языка. Классы называются Entity и Aggregate, а бизнес их не узнаёт. Решение: начинать со словаря и событий, структура папок подождёт.
  • Гигантский агрегат. Весь курс со студентами, уроками и платежами в одном корне, любая запись блокирует всё. Решение: агрегат хранит только те данные, что нужны для проверки одного правила, ссылки на другие агрегаты храним по идентификатору.
  • Одна модель на всю компанию. Попытка договориться об одном «студенте» для продаж и бухгалтерии. Решение: разные контексты и предохранительный слой между ними.
  • Логика снова в сервисах. Через полгода правила переезжают в контроллеры, и модель становится анемичной. Решение: ревью кода с вопросом «почему это правило не в агрегате».
  • Микросервис на каждую сущность. Граница сервиса должна совпадать с границей контекста. Иначе сетевые вызовы появятся там, где хватило бы вызова метода.
Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 700 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

Что почитать про Domain-Driven Design

Источник Для чего
Эрик Эванс, «Domain-Driven Design» (2003) первоисточник, тяжёлый, но полный
Вон Вернон, «Implementing Domain-Driven Design» (2013) следующий шаг после Эванса, больше практики, его советует Фаулер
DDD Reference (Эванс, 2015) бесплатный конспект всех паттернов, лицензия Creative Commons
Руководство Microsoft по DDD-микрослужбам на русском, слои домена, приложения и инфраструктуры на примере .NET

Читать лучше в таком порядке: справочник, чтобы запомнить термины, затем Вернон, а Эванса оставить на момент, когда появится свой проект для экспериментов.

Где освоить архитектуру бэкенда системно

DDD редко изучают отдельно от остального бэкенда: агрегаты бессмысленны без понимания транзакций, баз данных и интеграций. Путь от первого API до таких задач разобран в статье как стать backend-разработчиком, а ниже собраны курсы по бэкенд-разработке с программой по архитектуре.

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Backend-разработчик на PHP
Перейти на сайт курса
SkillFactorySkillFactory158 004 ₽4389 ₽/мес.12 месяцевОбзор курса
Backend-разработка на Node.js
Перейти на сайт курса
НетологияНетология45 000 ₽2500 ₽/мес.6 месяцевОбзор курса
Бэкенд-разработчик на Python
Перейти на сайт курса
SF EducationSF Education64 995 ₽3610 ₽/мес.3 месяцаОбзор курса
Профессия «Backend-разработчик на Go»
Перейти на сайт курса
SkillFactorySkillFactory122 040 ₽3390 ₽/мес.12 месяцевОбзор курса
Backend-разработчик на Kotlin
Перейти на сайт курса
OTUSOTUS48 000 ₽7800 ₽/мес.5 месяцевОбзор курса

Больше программ — в полном каталоге курсов по Backend-разработке

Если сначала хочется понять, чем занимается такой специалист и сколько он получает, загляните в обзор профессии бэкенд-разработчик и в разбор зарплат backend-разработчиков.

Часто задаваемые вопросы

Как расшифровывается DDD

DDD расшифровывается как Domain-Driven Design, по-русски предметно-ориентированное проектирование. Название взято из книги Эрика Эванса 2003 года, в которой подход описан набором паттернов.

Чем DDD отличается от чистой архитектуры

Чистая архитектура описывает слои приложения и направление зависимостей между ними. DDD отвечает на другой вопрос: как смоделировать бизнес и разделить его на контексты. Их часто используют вместе, и модель предметной области из DDD живёт во внутреннем слое.

Обязательно ли делать микросервисы при DDD

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

Что такое анемичная модель

Так Мартин Фаулер назвал модель, в которой объекты хранят только данные, а все правила вынесены в сервисы. Правило легко обойти, изменив поле напрямую, поэтому в DDD логику переносят внутрь агрегатов.

На каком языке программирования применяют DDD

На любом объектном языке: Java, C#, PHP, Python, JavaScript и TypeScript. Идеи подхода концептуальные, особенно стратегическая часть, поэтому от языка зависит только синтаксис.

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!