Доменно-ориентированное проектирование (DDD) в микросервисах
Когда организации переходят от монолитной архитектуры к микросервисам, они сталкиваются с критически важным вопросом: Как мы очерчиваем границы наших сервисов?
Теоретически микросервисы должны быть свободными, разделенными единицами, которые можно разрабатывать, развертывать и масштабировать независимо. Однако на практике многие команды в конечном итоге создают распределенный монолит — систему, в которой сервисы настолько тесно связаны, что одно изменение в бизнесе требует одновременного изменения и развертывания нескольких сервисов, что усугубляет задержки в сети и тупики при развертывании.
Чтобы избежать этой ловушки, архитекторы программного обеспечения обращаются к Domain-Driven Design (DDD). DDD, впервые представленный Эриком Эвансом в 2003 году, представляет собой методологию разработки программного обеспечения, которая согласовывает структуры кода со сложными бизнес-областями, которые они представляют.
В этой статье мы рассмотрим, как DDD предоставляет стратегические и тактические схемы для разработки чистых, изолированных и легко поддерживаемых микросервисов.
1. Стратегический план: определение границ услуг
DDD делится на два основных этапа: Стратегическое проектирование и Тактическое проектирование. Стратегический дизайн – это моделирование бизнес-контекста и определение границ услуг. Он обеспечивает «макро» представление об архитектуре.
А. Вездесущий язык
В рамках бизнеса разные отделы используют одни и те же слова для обозначения совершенно разных вещей. Например:
- Для команды продаж «Пользователь» — это ведущий или потенциальный клиент.
- Для команды безопасности «Пользователь» — это набор учетных данных для входа.
- Для команды доставки «Пользователь» — это физический адрес получателя.
Попытка создать единую, унифицированную модель базы данных для «Пользователя», удовлетворяющую всех, приводит к созданию огромной и запутанной базы кода. DDD решает эту проблему, создавая повсеместный язык — общий словарь, определенный и используемый как экспертами в области бизнеса, так и разработчиками в пределах определенных границ.
Б. Ограниченные контексты
Ограниченный контекст — это явная граница, внутри которой применяется модель предметной области. Внутри границы все термины вездесущего языка имеют одно и недвусмысленное значение.
Вместо одной модели «Пользователь» мы определяем отдельные модели в соответствующих ограниченных контекстах:
- В Контексте заказа у нас есть модель
Order, содержащая контактную информацию клиента. - В Контексте идентификации у нас есть модель
Credentials. - В разделе Контекст доставки у нас есть модель
DeliveryAddress.
Золотое правило микросервисов: Один ограниченный контекст напрямую соответствует одному микросервису.
Разделяя систему на ограниченные контексты, мы гарантируем, что микросервисы слабо связаны и представляют отдельные бизнес-возможности.
C. Сопоставление контекста: как взаимодействуют сервисы
Ограниченные контексты не существуют изолированно; они должны взаимодействовать. Карта контекстов определяет отношения и механизмы перевода между контекстами. Ключевые шаблоны включают в себя:
- Общее ядро: два контекста совместно используют небольшое подмножество модели предметной области и базы данных (обычно это не рекомендуется в микросервисах из-за связанности).
- Клиент-поставщик: один контекст (поставщик) должен предоставлять данные другому (клиенту). Поставщик должен согласовать графики выпуска с заказчиком.
- Антикоррупционный уровень (ACL): уровень трансляции, который преобразует входящие данные из внешней системы во внутреннюю модель домена клиента, не позволяя внешним моделям загрязнять внутреннюю архитектуру.
2. Тактический план: структурирование базы кода микросервиса
После того как границы сервиса определены с помощью стратегического проектирования, Тактическое проектирование предоставляет набор шаблонов проектирования для структурирования кода в рамках одного микросервиса.
A. Сущности и объекты значений
Внутри микросервиса модели делятся на две категории:
- Сущности: объекты, имеющие уникальную идентичность, которая сохраняется с течением времени, даже если их атрибуты меняются. Примеры:
OrderилиProduct. - Объекты значений: объекты, которые не имеют уникальной идентичности и полностью определяются своими атрибутами. Они неизменны. Примеры:
ShippingAddressилиProductDimensions. Если два объекта-значения имеют одинаковые атрибуты, они считаются равными.
B. Агрегаты и корни агрегатов
Агрегат — это кластер связанных сущностей и объектов-значений, которые рассматриваются как единая единица при изменении данных. Каждый агрегат имеет Корень агрегата, который является единственной точкой входа, через которую внешние объекты могут взаимодействовать с агрегатом. Корень гарантирует соблюдение всех бизнес-инвариантов (правил).
Например, на схеме выше:
- В Службе заказов
Orderявляется совокупным корнем. Он содержитOrderItem(сущность) иShippingAddress(объект значения). Внешние службы не могут изменятьOrderItemнапрямую; они должны вызвать метод в корнеOrder(например,order.AddItem()), который проверяет, что заказ еще не отправлен.
C. События домена
Событие предметной области – это событие, произошедшее в сфере, которая волнует бизнес-экспертов. Он используется для асинхронной передачи изменений в ограниченных контекстах.
Когда агрегат выполняет команду, он публикует событие домена (например, OrderCreated). Другие службы прослушивают это событие и соответствующим образом обновляют свое состояние, обеспечивая конечную согласованность без синхронных зависимостей API.
3. Конкретный пример: служба заказов и служба инвентаризации
Давайте посмотрим на упрощенную реализацию тактического DDD в Go для Службы заказов, показывающую, как совокупный корень координирует бизнес-правила.
package domain
import (
"errors"
"time"
)
// Value Object (Immutable, identity-less)
type ShippingAddress struct {
Street string
City string
ZipCode string
}
// Entity (Has identity, mutable)
type OrderItem struct {
ProductID string
Quantity int
Price float64
}
// Aggregate Root (Enforces transactional boundaries)
type Order struct {
ID string
Items []OrderItem
Address ShippingAddress
Status string
CreatedAt time.Time
}
// NewOrder creates a new Order Aggregate Root
func NewOrder(id string, address ShippingAddress) *Order {
return &Order{
ID: id,
Items: []OrderItem{},
Address: address,
Status: "PENDING",
CreatedAt: time.Now(),
}
}
// AddItem enforces business rules before mutating state
func (o *Order) AddItem(productID string, qty int, price float64) error {
if o.Status != "PENDING" {
return errors.New("cannot add items to a finalized or cancelled order")
}
if qty <= 0 {
return errors.New("quantity must be greater than zero")
}
o.Items = append(o.Items, OrderItem{
ProductID: productID,
Quantity: qty,
Price: price,
})
return nil
}
Когда заказ успешно сохранен, сервис публикует событие в брокере сообщений:
{
"event_id": "evt_98231",
"event_type": "OrderCreated",
"timestamp": "2026-06-26T00:15:00Z",
"payload": {
"order_id": "ord_5521",
"items": [
{ "product_id": "prod_88", "quantity": 2 }
]
}
}
Служба инвентаризации прослушивает это событие, обновляет свой локальный объект StockLevel и завершает поток асинхронно.
4. Стратегическое и тактическое DDD в микросервисах
| Фаза/Аспект | Стратегический DDD | Тактический DDD |
|---|---|---|
| Объем | Глобальная архитектура системы (макро) | Внутри единой кодовой базы сервиса (микро) |
| Основная цель | Нарисуйте четкие, несвязанные границы услуг | Моделируйте богатую бизнес-логику и применяйте инварианты |
| Основные понятия | Ограниченные контексты, вездесущий язык, карта контекстов | Сущности, объекты значений, агрегаты, события предметной области |
| Аудитория | Архитекторы, менеджеры по продуктам, разработчики | Разработчики программного обеспечения, рецензенты кода |
| Влияние на микросервисы | Определяет количество и объем услуг | Определяет транзакции базы данных и структуру каталогов |
Вывод: сначала начните со стратегического DDD
Применение доменно-ориентированного проектирования к микросервисам — это мощный способ обеспечить соответствие вашей архитектуры структуре вашего бизнеса. Однако команды часто совершают ошибку, уделяя слишком много внимания тактическим шаблонам (например, написанию интерфейсов репозитория и ценных объектов), игнорируя при этом стратегический дизайн.
Если вы не определите границы правильно, никакое количество чистого тактического кода не спасет вашу систему от превращения в распределенный монолит. Всегда начинайте со стратегического картирования — определите свой вездесущий язык, сгруппируйте логику в ограниченные контексты, составьте карту коммуникационных потоков и позвольте границам ваших микросервисов естественным образом возникнуть из этих определений.