DDD (Domain-Driven Design, предметно-ориентированное проектирование) — подход к архитектуре, при котором код строят вокруг модели бизнеса, а не вокруг таблиц базы данных и фреймворка. Разработчики и эксперты предметной области договариваются об общем языке, делят систему на ограниченные контексты и прячут бизнес-правила внутрь объектов, которые сами следят за своей целостностью. Это один из способов выстроить архитектуру приложения там, где логики много и она часто меняется.
Ниже разберём термины на одном сквозном примере, перепишем анемичную модель в агрегат на JavaScript и покажем, когда DDD не нужен вовсе.
Что такое 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-проектах: у каждого объекта одна понятная ответственность.
Пример 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 или другой базы, а код агрегата при этом не меняется.
Как внедрить DDD в проект: пошагово
Начинать с паттернов и папок domain/ бесполезно. Порядок обычно такой:
- Соберите людей, которые знают бизнес: руководителя направления, операторов, бизнес-аналитика. Без эксперта предметной области получится DDD только по названиям классов.
- Выпишите события процесса в прошедшем времени: «заявка оставлена», «договор подписан», «студент записан». На стикерах или в онлайн-доске.
- Найдите места, где слова меняют смысл. Там и проходят границы контекстов.
- Нарисуйте карту контекстов и подпишите, кто от кого зависит.
- Выберите главную предметную область (core domain), ради которой бизнес существует. Тактические паттерны тратим на неё, а типовые вещи вроде авторизации берём готовыми.
- Заведите словарь единого языка и правьте его при каждом новом термине.
- Рефакторьте по одному агрегату. Переписывать всю систему разом рискованно, удобнее переносить правила внутрь объектов постепенно.
Карту контекстов и схемы агрегатов удобно рисовать в нотации UML или C4. Такие схемы каждый день обсуждает с бизнесом и командой архитектор ПО.
Когда DDD не нужен и частые ошибки

Фаулер подчёркивает, что подход особенно подходит для сложных предметных областей с запутанной логикой. Обратная сторона: в простом проекте он добавит слоёв и замедлит команду.
DDD избыточен, если приложение почти целиком CRUD (создать, прочитать, обновить, удалить), бизнес-правил единицы, а эксперта предметной области в команде нет.
Ошибки, которые встречаются у новичков чаще всего:
- Паттерны без языка. Классы называются
EntityиAggregate, а бизнес их не узнаёт. Решение: начинать со словаря и событий, структура папок подождёт. - Гигантский агрегат. Весь курс со студентами, уроками и платежами в одном корне, любая запись блокирует всё. Решение: агрегат хранит только те данные, что нужны для проверки одного правила, ссылки на другие агрегаты храним по идентификатору.
- Одна модель на всю компанию. Попытка договориться об одном «студенте» для продаж и бухгалтерии. Решение: разные контексты и предохранительный слой между ними.
- Логика снова в сервисах. Через полгода правила переезжают в контроллеры, и модель становится анемичной. Решение: ревью кода с вопросом «почему это правило не в агрегате».
- Микросервис на каждую сущность. Граница сервиса должна совпадать с границей контекста. Иначе сетевые вызовы появятся там, где хватило бы вызова метода.
Что почитать про 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 Перейти на сайт курса | 158 004 ₽ | 4389 ₽/мес. | 12 месяцев | Обзор курса | |
| Backend-разработка на Node.js Перейти на сайт курса | 45 000 ₽ | 2500 ₽/мес. | 6 месяцев | Обзор курса | |
| Бэкенд-разработчик на Python Перейти на сайт курса | 64 995 ₽ | 3610 ₽/мес. | 3 месяца | Обзор курса | |
| Профессия «Backend-разработчик на Go» Перейти на сайт курса | 122 040 ₽ | 3390 ₽/мес. | 12 месяцев | Обзор курса | |
| Backend-разработчик на Kotlin Перейти на сайт курса | 48 000 ₽ | 7800 ₽/мес. | 5 месяцев | Обзор курса |
Больше программ — в полном каталоге курсов по Backend-разработке
Если сначала хочется понять, чем занимается такой специалист и сколько он получает, загляните в обзор профессии бэкенд-разработчик и в разбор зарплат backend-разработчиков.




