Разработка 8 июня 2026 · 13 мин чтения 337 0

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

  1. Разработчики и domain experts работают вместе над моделью
  2. Каждый термин фиксируется с строгим определением
  3. Bounded Context определяет scope языка — за его границами те же слова могут значить другое
  4. Glossary поддерживается как living документ
  5. Код использует те же названия (классы, методы, поля)
  6. Документация, тесты, 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 становятся микросервисами». Это упрощение, но в нём есть зерно истины.

Правильный подход

  1. Сначала identify bounded contexts через DDD-анализ
  2. Понять зависимости между контекстами
  3. Только потом decide, развёртывать ли каждый context как отдельный сервис
  4. Многие контексты остаются модулями в 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, не в коде.