DDD-спецификация: гайд для архитектора

Архитектурные решения в DDD: контексты, агрегаты, Outbox, Saga.

DDD гайд архитектор

DDD гайд архитектор: введение

DDD гайд архитектор — архитектор участвует в 6 из 16 разделов DDD-спецификации: Bounded Context, Domain Model, Domain Events, Saga, Интеграции и НФТ. Его задача -- принять ключевые технические решения, определить границы системы, паттерны взаимодействия и инфраструктурную стратегию.

Этот гайд собирает все архитектурные решения из Шаблона спецификации и группирует их по темам. Все примеры используют единый домен -- "Интернет-магазин: оформление заказа".


1. Bounded Context

Bounded Context определяет границу развертывания и автономности. Архитектор отвечает за классификацию поддомена, выбор паттернов связи и проектирование защитных слоев.

Тип поддомена

Определите, к какому типу относится контекст:

  • Core -- ключевая бизнес-ценность. Заказы в интернет-магазине -- Core: здесь конкурентное преимущество. Это оправдывает максимальные инвестиции в качество кода, покрытие тестами и архитектурную чистоту.
  • Supporting -- поддержка основного бизнеса, но без уникальной логики.
  • Generic -- стандартная функциональность, которую можно купить или использовать as-is.

Паттерны интеграции

Для каждого соседнего контекста выберите паттерн связи:

КонтекстПаттернОбоснование
Каталог товаровACLВнешняя система, чужая модель данных -- изолируем через адаптер
ОплатаCustomer-SupplierМы -- customer, зависим от их API
ДоставкаCustomer-SupplierМы -- customer, отправляем события через Kafka
СкладPartnershipСовместная работа: резервирование товара

Границы развертывания

Определите маппинг: один микросервис = один Bounded Context или несколько контекстов в одном модулите. Для домена заказов один контекст = один deployable unit.

Антикоррупционный слой (ACL)

Для каждой внешней системы нужен адаптер, изолирующий доменную модель от чужих изменений. Пример: CatalogServiceAdapter преобразует ответ каталога в ProductSnapshot, защищая домен заказов от изменений в API каталога.

Context Map

Постройте полную карту контекстов с направлениями зависимостей. Диаграмма C1 (System Context) визуализирует систему как "черный ящик" с пользователями и внешними системами вокруг нее.


2. Domain Model

Доменная модель -- центральное архитектурное решение. Архитектор определяет границы агрегатов, выбирает между VO и Entity, решает вопросы идентификации и конкурентного доступа.

Границы агрегатов

Order -- корень агрегата, OrderItem -- внутренняя сущность. Все изменения идут через Order. Инварианты (бизнес-правила BR-1..BR-6) проверяются в одном месте -- в агрегате.

Правило: агрегат должен быть достаточно маленьким для атомарного сохранения, но достаточно большим для защиты инвариантов.

Value Objects vs Entities

Разделение по критерию идентичности:

  • Value Objects (нет жизненного цикла): Money, DeliveryAddress, ProductSnapshot. Два объекта Money(100, RUB) эквивалентны.
  • Entities (есть идентификатор): OrderItem -- у каждой позиции уникальный OrderItemId.

ProductSnapshot vs ссылка на каталог

Снимок цены (ProductSnapshot) фиксируется при создании заказа. Альтернатива -- хранить только productId -- создает runtime-зависимость от доступности каталога при каждом чтении заказа. Снимок обеспечивает автономность контекста заказов.

Типизированные идентификаторы

Используйте OrderId, OrderItemId, CustomerId вместо голого UUID. Это предотвращает ошибки передачи ID не того типа на этапе компиляции.

Optimistic locking

Заказ с 50+ позициями -- рассмотреть optimistic locking через поле version. При конкурентном изменении одного заказа (например, покупатель добавляет товар, пока менеджер меняет статус) проигравшая транзакция получит ошибку и должна повторить операцию.


3. Domain Events

События -- клей между контекстами. Архитектор определяет тип каждого события, механизм доставки и гарантии обработки.

Внутренние vs внешние события

  • Внутренние -- обрабатываются синхронно внутри контекста. Пример: ItemAdded -- пересчет итоговой суммы внутри агрегата Order.
  • Внешние -- передаются через Kafka для других контекстов. Пример: OrderCreated -- Склад резервирует товар, Уведомления отправляют подтверждение покупателю.

Transactional Outbox

Гарантия доставки внешних событий: событие сохраняется в таблицу domain_events_outbox в одной транзакции с агрегатом. Отдельный процесс (Debezium CDC или polling) читает таблицу и отправляет в Kafka. Это исключает ситуацию "агрегат сохранен, а событие потеряно".

Формат и версионирование

JSON Schema с явной версией в каждом событии. Правило эволюции: новые поля добавлять, старые не удалять (backward compatibility). Используйте Schema Registry для валидации совместимости при деплое.

Ordering

Kafka partition key = orderId. Это гарантирует, что все события одного заказа попадают в одну партицию и обрабатываются в порядке возникновения: OrderCreated всегда придет раньше OrderPaid.

Идемпотентность подписчиков

При at-least-once доставке одно событие может прийти повторно. Каждый подписчик дедуплицирует по eventId. Пример: Склад не должен резервировать товар дважды при повторной доставке OrderCreated.


4. Saga

Saga -- самый сложный архитектурный элемент. Архитектор выбирает стиль координации, проектирует хранение состояния и стратегию компенсаций.

Оркестрация vs хореография

Для домена заказов выбрана оркестрация (Saga Orchestrator) -- один компонент управляет последовательностью шагов. Это дает четкую видимость процесса и упрощает отладку. Хореография (каждый сервис реагирует на события) лучше подходит для простых цепочек без ветвления.

Пример: Saga "Обработка заказа" -- Склад (резерв) -> Оплата (платеж) -> Доставка (заявка). При ошибке оплаты -- компенсация: Склад отменяет резерв.

Saga state

Оркестратор хранит текущий шаг в БД: sagaId, sagaType, currentStep, payload, startedAt. При падении приложения Saga восстанавливается из последнего сохраненного состояния и продолжает с прерванного шага.

Компенсации

Каждый шаг Saga, изменяющий состояние внешней системы, должен иметь компенсирующее действие. Компенсация -- не откат, а обратная бизнес-операция:

ШагДействиеКомпенсация
СкладРезервировать товарОтменить резерв
ОплатаСоздать платежВернуть средства
ДоставкаСоздать заявкуТолько логирование (создать вручную позже)

Таймауты и retry

Exponential backoff для retry: 1с -> 2с -> 4с. После 3 неудачных попыток -- компенсация + алерт операционной команде. Таймауты: 30 секунд на резерв и оплату, 5 минут на создание доставки. Две Saga вместо одной: обработка заказа и возврат -- разные процессы с разными компенсациями.


5. Интеграции

Интеграции -- ключевые точки отказа. Архитектор проектирует защиту от сбоев внешних систем и стратегию контрактного тестирования.

Синхронные vs асинхронные

  • Синхронные (REST): Каталог, Оплата, Склад. Требуют circuit breaker, fallback, timeout. Пример: запрос к Каталогу с SLA <= 300ms, при недоступности -- fallback на Redis-кэш (TTL 30 мин).
  • Асинхронные (Kafka): Доставка, Уведомления. Transactional Outbox гарантирует доставку, partition key = orderId гарантирует порядок.

ACL (Anti-Corruption Layer)

Для каждого внешнего API -- отдельный адаптер. При изменении внешнего API меняется только адаптер, доменная модель остается нетронутой. Пример: CatalogServiceAdapter преобразует DTO каталога в ProductSnapshot.

Circuit breaker

Resilience4j для синхронных интеграций. Состояния: CLOSED (нормальная работа) -> OPEN (внешняя система упала, все запросы идут в fallback) -> HALF_OPEN (пробные запросы). Для каждого внешнего сервиса -- свой circuit breaker с настроенными порогами.

Contract testing

Consumer-driven contract tests (Pact или Spring Cloud Contract). Контекст заказов как consumer определяет ожидания от API каталога. При изменении API каталога тесты ломаются до деплоя, а не в production.


6. Нефункциональные требования

НФТ определяют инфраструктурные решения. Архитектор переводит бизнес-ожидания в конкретные технические меры.

Кэширование

Целевое p95 <= 200ms требует кэширования на нескольких уровнях: Redis для данных каталога (TTL 30 мин), кэш прочитанных заказов (TTL 5 мин с инвалидацией при обновлении), connection pooling (HikariCP/PgBouncer).

Масштабирование

x5 нагрузка в период распродаж (до 250 rps на оформление заказа) -- горизонтальное масштабирование. HPA (Horizontal Pod Autoscaler) в Kubernetes по CPU и custom metrics (rps). Rate limiting на API Gateway для защиты от перегрузки. Доступность 99.9% -- минимум 2 реплики приложения, health checks, graceful shutdown.

PCI DSS

Данные банковских карт не хранить и не логировать. Tokenization через платежный шлюз: контекст заказов работает только с токенами, реальные данные карт никогда не попадают в периметр системы.

Партиционирование и архивация

1 000 000 заказов в год (~10 ГБ/год): партиционирование таблицы orders по дате создания. Завершенные заказы старше 3 лет -- в архивное хранилище. Read-реплика PostgreSQL для тяжелых запросов SearchOrders. Outbox-таблица растет быстро -- очистка опубликованных событий старше 7 дней.