Domain-Driven Design: bounded contexts, aggregates, CQRS
Domain-Driven Design — подход к проектированию сложных программных систем, сформулированный Эриком Эвансом в книге 2003 года «Domain-Driven Design: Tackling Complexity in the Heart of Software». За двадцать лет DDD прошёл путь от нишевой методологии для enterprise-разработки до фундаментальной концепции, лежащей в основе многих современных архитектурных подходов: микросервисов, event sourcing, CQRS. Параллельно появилось множество поверхностных интерпретаций, превращающих DDD в набор tactical patterns без понимания основной философии.
Эта статья описывает DDD как целостный подход: его базовые концепции (ubiquitous language, bounded contexts), tactical patterns (entities, value objects, aggregates), связь с современной архитектурой микросервисов, типичные ошибки внедрения, сценарии, где DDD подходит и где излишен. Понимание DDD не делает каждый проект Domain-Driven, но даёт инструменты для проектирования систем со сложной бизнес-логикой.
Что такое DDD
Domain-Driven Design — это методология проектирования программного обеспечения, ставящая в центр модель предметной области (domain). Базовая идея: качество системы определяется не технической архитектурой, а тем, насколько точно код отражает бизнес-реальность, для которой написан.
Фундаментальные принципы
- Domain (предметная область) — главное, остальное — детали
- Модель должна быть результатом сотрудничества разработчиков и domain experts
- Один общий язык для бизнеса и кода (Ubiquitous Language)
- Большая система разбивается на изолированные Bounded Contexts
- Внутри каждого контекста — rich domain model с поведением, не anemic data structures
- Continuous refactoring модели по мере growing понимания domain
Эрик Эванс и истоки
Эрик Эванс работал консультантом на сложных проектах в 1990-х. Его frustration с тем, что большинство архитектурных подходов фокусировались на технических аспектах (database design, layered architecture, components), но игнорировали бизнес-сложность, привела к формулированию DDD как реакции на эти проблемы.
Книга «DDD» (часто называемая «Blue Book») стала foundational text для нескольких поколений разработчиков. Параллельные книги — «Implementing Domain-Driven Design» Вон Вернона (2013) с практическими примерами, «Domain-Driven Design Distilled» 2016 года для быстрого старта.
Strategic vs Tactical DDD
DDD делится на две большие части с разными уровнями абстракции.
| Тип | Фокус | Аудитория |
|---|---|---|
| Strategic DDD | Большая картина: контексты, их связи, границы | Архитекторы, tech leads, продакт-менеджеры |
| Tactical DDD | Implementation patterns внутри контекста | Разработчики |
Многие команды учат tactical patterns (entities, repositories, services) и называют это «делаем DDD». Это ошибка — без strategic части tactical patterns превращаются в набор boilerplate без реальных преимуществ.
Ubiquitous Language
Ubiquitous Language — общий vocabulary, используемый и domain experts (бизнес-сторона), и разработчиками. Один и тот же термин означает одно и то же в обсуждениях, документации, коде.
Почему это критично
Классическая проблема в разработке: бизнес говорит «клиент», разработчики реализуют User в базе данных, в API это Customer, в analytics — Account. Каждое название имеет свои сloven nuances. Когда баг происходит, никто не знает, какая именно сущность затронута. Coordination между командами становится упражнением в переводе.
Ubiquitous Language решает это через жёсткое требование: используем один язык везде. «Клиент» — это конкретная сущность, которая в коде называется Customer, в БД — customers, в API endpoints — /customers, в discussions — Customer. Нет других названий, нет синонимов, нет приближений.
Как формируется Ubiquitous Language
- Разработчики и domain experts работают вместе над моделью
- Каждый термин фиксируется с строгим определением
- Bounded Context определяет scope языка — за его границами те же слова могут значить другое
- Glossary поддерживается как living документ
- Код использует те же названия (классы, методы, поля)
- Документация, тесты, commit messages — на том же языке
Anti-patterns
- Перевод бизнес-терминов на «технический» язык в коде
- Использование generic-имён (Entity, Manager, Service без context)
- Документация на одном языке, код — на другом
- Multiple terms для одного концепта в разных частях системы
- Игнорирование точных определений domain experts «потому что разработчикам так удобнее»
Bounded Contexts
Bounded Context — концептуальная и физическая граница, внутри которой действует определённая модель и язык. Каждая large system естественно делится на несколько bounded contexts с разными моделями одних и тех же сущностей.
Пример с e-commerce
Сущность «Order» в e-commerce-системе означает разные вещи в разных контекстах:
| Context | Order означает | Главные атрибуты |
|---|---|---|
| Sales | Покупка с ценой и customer | Total, discount, customer, items |
| Inventory | Резервирование товаров | Item IDs, quantities, warehouse |
| Shipping | Физическая отправка | Address, weight, carrier, tracking |
| Accounting | Financial transaction | Amount, tax, payment method, invoice |
| Customer Service | Историческая запись для support | Status history, communications |
Попытка создать «универсальный Order» для всех контекстов приводит к monster-сущности с десятками полей, большая часть из которых не имеет смысла в каждом конкретном случае. Каждый контекст имеет свою собственную модель Order, оптимальную для своих задач.
Границы контекстов
Bounded Contexts определяются естественными границами в бизнесе и команде:
- Разные бизнес-capabilities (sales, fulfillment, support)
- Разные user groups с разными ментальными моделями
- Разные команды разработки
- Разные технологические requirements (transactional vs analytical)
- Регуляторные boundary (compliance-зоны)
Context Map
Context Map — визуализация bounded contexts и связей между ними. Описывает, как контексты взаимодействуют: какие данные передают, кто зависит от кого, какие patterns integration используются.
Типы integration patterns
| Pattern | Описание |
|---|---|
| Shared Kernel | Несколько контекстов разделяют небольшой общий kernel-код |
| Customer-Supplier | Один контекст — поставщик данных/услуг для другого |
| Conformist | Downstream-контекст принимает модель upstream без изменений |
| Anti-Corruption Layer | Translation слой между контекстами с разными моделями |
| Open Host Service | Публичный API для intergration с многими клиентами |
| Published Language | Стандартный формат коммуникации между контекстами |
| Separate Ways | Контексты не интегрируются, дублируют функциональность независимо |
| Big Ball of Mud | Anti-pattern: контексты перемешаны без чёткой структуры |
Anti-Corruption Layer
Особенно важный pattern — Anti-Corruption Layer (ACL). При integration с legacy-системой или сторонним API ACL «переводит» их модель в нашу. Это защищает наш domain model от «загрязнения» внешними concepts.
Пример: интегрируемся с legacy-CRM, имеющей странную модель «Lead» с 50 полями. ACL принимает Lead из CRM, преобразует в нашу clean модель Customer, использует только нужные поля. Если CRM изменится, мы меняем только ACL, не весь codebase.
Tactical Patterns DDD
Внутри bounded context используются специфические patterns для implementation domain model.
Entity
Объект с identity, persisting через время. Two Order объекта с разными ID — два разных Entity, даже если их полные содержимое идентично. Identity обычно — UUID или database ID.
class Order {
private final OrderId id; // identity
private OrderStatus status;
private List<OrderItem> items;
public void cancel() {
if (status != PLACED) throw new IllegalStateException();
this.status = CANCELLED;
}
}
Value Object
Объект без identity, defined только своими атрибутами. Two Money(100, USD) объекта — концептуально один и тот же. Value Objects immutable — изменение возвращает новый объект.
class Money {
private final BigDecimal amount;
private final Currency currency;
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException();
}
return new Money(amount.add(other.amount), currency);
}
}
Aggregate
Cluster связанных Entities и Value Objects, обрабатываемый как единое целое. Имеет один Aggregate Root — Entity, через который происходит весь доступ к содержимому Aggregate.
Aggregate boundaries определяют transactional consistency: всё внутри aggregate обновляется атомарно, между aggregates — eventual consistency.
// Order — Aggregate Root
// OrderItem — внутри Aggregate, доступ только через Order
class Order {
private final OrderId id;
private final List<OrderItem> items = new ArrayList<>();
public void addItem(ProductId product, int quantity, Money price) {
// Бизнес-правила: проверки, что item можно добавить
if (items.size() >= MAX_ITEMS) throw new ...;
items.add(new OrderItem(product, quantity, price));
}
public Money getTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
Domain Service
Операции, не принадлежащие явно к одной Entity или Value Object. Обычно когда операция вовлекает несколько aggregates.
class TransferService {
public void transfer(Account from, Account to, Money amount) {
from.withdraw(amount);
to.deposit(amount);
}
}
Repository
Абстракция для access к persistence. Скрывает детали БД от domain layer. Repository обычно соответствует Aggregate — один на каждый тип Aggregate Root.
interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
List<Order> findByCustomer(CustomerId customer);
}
Domain Event
Объект, представляющий что-то significant, произошедшее в domain. OrderPlaced, PaymentReceived, CustomerRegistered. Используется для:
- Уведомления других aggregates о изменениях
- Integration с другими bounded contexts
- Triggering side effects (отправка emails, аналитика)
- Audit log
- Event sourcing
Factory
Pattern для создания сложных объектов. Используется когда constructor с десятками параметров становится unwieldy, или нужна сложная инициализация.
class OrderFactory {
public Order createFromCart(Cart cart, Customer customer) {
Order order = new Order(generateId(), customer.getId());
for (CartItem item : cart.getItems()) {
order.addItem(item.getProduct(), item.getQuantity(), item.getPrice());
}
return order;
}
}
CQRS — Command Query Responsibility Segregation
CQRS — pattern, часто используемый вместе с DDD. Разделяет операции на commands (changes state) и queries (returns data). Каждая часть оптимизируется независимо.
Зачем нужен CQRS
- Read-нагрузка обычно намного выше write — разное scaling
- Read-модели можно денормализовать для производительности
- Write-модели сохраняют complex business logic и invariants
- Разные команды могут работать над разными частями
Архитектура CQRS
UI / API
|
+-- Commands ---> Command Handler ---> Domain Model (writes)
| |
| Domain Events
| |
+-- Queries ---> Read Model Projection Updates
(reads) |
Read Database
Event Sourcing
Event Sourcing — pattern, при котором state системы хранится не как current snapshot, а как sequence of events. Текущий state восстанавливается воспроизведением events.
Преимущества
- Полная audit история без специальной работы
- Возможность восстановить state на любой момент в прошлом
- Естественная integration с event-driven architectures
- Возможность создавать новые projections из старых events
- Debugging через replay events
Недостатки
- Сложность query — нужны projections для эффективного чтения
- Storage растёт постоянно
- Schema evolution events — отдельная проблема
- Eventual consistency между write и read models
- Не подходит для всех типов данных
Event sourcing подходит для
- Финансовые системы с требованием полного audit
- E-commerce заказы с complex lifecycle
- Регулируемые индустрии (медицина, юридические системы)
- Systems with complex business processes (insurance claims, mortgages)
DDD и микросервисы
Связь между DDD и микросервисами часто формулируется как «bounded contexts становятся микросервисами». Это упрощение, но в нём есть зерно истины.
Правильный подход
- Сначала identify bounded contexts через DDD-анализ
- Понять зависимости между контекстами
- Только потом decide, развёртывать ли каждый context как отдельный сервис
- Многие контексты остаются модулями в monolith — это OK
Антипаттерн «микросервис на каждый класс»
Некоторые команды интерпретируют микросервисы как extreme decomposition — separate сервис для каждой Entity. Это создаёт массу проблем:
- Network latency между сервисами
- Сложность distributed transactions
- Operational overhead managing десятки малых сервисов
- Aggregate boundaries разорваны через services
Правильное число микросервисов обычно соответствует количеству bounded contexts, не количеству entities. Несколько entities внутри одного context — нормально для одного сервиса.
Когда DDD подходит
Хорошие кандидаты для DDD
- Сложная бизнес-логика, не CRUD-приложение
- Domain experts доступны для общения с командой
- Большая, долгоживущая система
- Сложная регуляторная среда (банки, медицина, finance)
- Команда из senior разработчиков, готовых learn
- Долгосрочное commitment к проекту
- Multiple bounded contexts в одной системе
Когда DDD излишен
- Простой CRUD-сайт или API
- Прототип или MVP
- Маленький проект, который не вырастет
- Команда без опыта DDD и без mentor
- Нет доступа к domain experts
- Простой domain без скрытой сложности
- Краткосрочные проекты
Применение DDD к простому проекту создаёт ненужную complexity без real benefit. «Заворачивать» CRUD-операции в Aggregates, Repositories, Domain Services добавляет boilerplate без architectural improvement.
Типичные ошибки при внедрении DDD
Анемичная модель
Самая частая ошибка — создание Entities как набора getters/setters без поведения, с бизнес-логикой в сервисах. Это противоречит DDD-философии: rich domain model с поведением внутри.
«Анемичная» модель неотличима от ORM-сгенерированной структуры данных. DDD здесь не даёт никакой пользы. Поведение должно жить с данными, которыми оно манипулирует.
Игнорирование Strategic DDD
Многие команды учат tactical patterns и applies их без понимания strategic части. Bounded contexts, context maps, ubiquitous language — strategic patterns дают основную ценность DDD. Без них tactical patterns — это набор шаблонов.
Premature DDD
Внедрение DDD на самой ранней стадии проекта, когда domain ещё не понятен, контексты не определены. Это приводит к over-engineering и rigid модели, которую сложно менять.
DDD хорошо работает, когда команда уже имеет понимание domain и видит сложность, требующую structured подхода. Для новых доменов сначала explore через простой код, потом introduce DDD по мере роста сложности.
Слепое следование book examples
Book examples DDD часто использовать упрощённые сценарии. Real-world применение требует адаптации, не копирования. Каждый domain имеет свои особенности, не каждый pattern из books подходит.
DDD без domain experts
DDD требует постоянного взаимодействия с people, понимающими domain. Без этого Ubiquitous Language формируется из инженерных guesses, не из реальной бизнес-реальности. Результат — модель, выглядящая как DDD, но не соответствующая domain.
Tools over thinking
Фокус на frameworks и tools, поддерживающих DDD (Axon, EventStore, специальные ORM-подходы), вместо на думинiнgе о domain. Tools могут помогать, но они не заменяют work understanding business.
DDD — это не о специальных классах и patterns. Это о думанiи. Команды, которые «делают DDD» через шаблоны без understanding философии, получают сложность без benefits. Команды, понимающие принципы и applying их judiciously, строят maintainable системы для сложных доменов.
Современное состояние DDD
DDD в 2026 году
DDD остаётся актуальным, но эволюционировал:
- Event-driven architectures (Kafka, EventBridge) делают integration между bounded contexts более natural
- Микросервисная архитектура mainstream, что делает strategic DDD более practical
- Cloud-native patterns (sagas, event sourcing) тесно связаны с DDD
- Functional programming languages (Scala, F#, Elixir) дают новые способы expressing domain models
- Type systems modern languages (TypeScript, Kotlin, Rust) поддерживают rich domain modeling
Event Storming
Альберто Брандолини предложил Event Storming — collaborative workshop для discovering domain. Stakeholders и developers вместе делают brainstorming events системы, формируя context maps.
Event Storming стал популярным инструментом для start DDD-проектов. Активная visual collaboration ускоряет понимание domain.
DDD-related books
- «Domain-Driven Design» Эрика Эванса — original Blue Book
- «Implementing Domain-Driven Design» Вон Вернона — практические примеры (Red Book)
- «Domain-Driven Design Distilled» Вон Вернона — короткое введение
- «Patterns, Principles, and Practices of Domain-Driven Design» Скотта Миллетта — comprehensive
- «Learning Domain-Driven Design» Влада Хононова — современный взгляд
DDD vs другие подходы
| Подход | Фокус | Когда лучше DDD |
|---|---|---|
| Transaction Script | Procedural обработка request | Простая бизнес-логика |
| Table Module | Один класс на таблицу | CRUD-приложения |
| Active Record | ORM с поведением в data classes | Средняя сложность, Ruby on Rails-style |
| Functional/CQRS | Functions over data | Когда event sourcing удобен |
| Event-driven | System reacts to events | Сложно подобрать без DDD-подхода к границам |
DDD не исключает other подходы — внутри bounded contexts можно use разные стили в зависимости от сложности конкретного context.
Часто задаваемые вопросы
Подходит ли DDD для стартапа
На самой ранней стадии MVP — обычно нет, преждевременная оптимизация. Когда продукт находит market fit и domain становится понятнее, DDD-thinking помогает. Но full DDD-implementation редко оправдан до scale, требующего серьёзной structure. Strategic DDD-thinking — bounded contexts, ubiquitous language — useful с раннего этапа без full tactical implementation.
Можно ли использовать DDD с микросервисами
Это natural fit, но не automatic. Bounded contexts помогают определить sensible service boundaries. Однако не каждый bounded context должен быть отдельным микросервисом — некоторые остаются модулями в monolith. Granularity microservices — отдельное архитектурное decision поверх DDD.
Нужна ли отдельная database для каждого bounded context
В теории — да, это идеальное изоляция. На практике одна database с разными schemas/tables на context часто достаточно. Полная физическая изоляция databases оправдана при действительно независимых командах и контекстах с разными scaling requirements.
Как объяснить DDD не-техническим stakeholders
Через benefit: «мы используем тот же язык, что вы. Когда вы говорите Order, мы реализуем Order — без перевода. Это снижает misunderstandings и ускоряет development.» Не углубляться в технические детали — фокусироваться на business benefits: точность requirements, скорость изменений, понятность кода для new team members.
Что делать с legacy-системой, которая хочется DDD-ifyм
Полный переписывание редко оправдан. Strangler pattern: новые features писать с DDD-подходом, постепенно extracting bounded contexts из legacy. Anti-Corruption Layer защищает new code от legacy концепций. Долгосрочный путь — годы, не месяцы.
Какие frameworks помогают с DDD
Axon Framework (Java) — purpose-built для DDD + CQRS + Event Sourcing. Spring Modulith для modular monolith с DDD-flavor. EventStoreDB для event sourcing. Но frameworks не делают DDD — thinking and modeling делают. Frameworks упрощают tactical implementation.
Заключение
Domain-Driven Design — мощный подход к проектированию сложных систем, центрирующий work вокруг бизнес-понимания, а не технических деталей. Strategic DDD (bounded contexts, ubiquitous language, context maps) даёт фундамент для архитектурных decisions. Tactical patterns (entities, aggregates, value objects, domain services) помогают implementing rich domain model внутри контекстов.
Главные практические рекомендации: focus на strategic DDD прежде tactical patterns; работать с domain experts continuously; начинать с простого, добавлять DDD по мере роста сложности; не применять к простым CRUD-проектам; не путать «делать DDD» с создание boilerplate Repositories и Services. Современная архитектура микросервисов и event-driven подходов делает DDD более relevant, чем когда-либо — bounded contexts становятся естественной basis для distributed system design. Команды, инвестирующие в подлинное понимание DDD, строят systems, остающиеся maintainable годами. Те, кто следует «DDD-фестонам» без понимания философии, получают сложность без benefits. Difference между ними — в думнании о domain, не в коде.