Ограниченные контексты и карта контекстов
В прошлой статье мы разобрали домен, поддомены и единый язык. Осталась неудобная деталь: единый язык не бывает единым на всю компанию. Стоит собрать всех — продажи, склад, бухгалтерию, поддержку — и попросить дать одно определение слова «заказ», как переговоры затянутся на месяцы и закончатся определением, которое не нужно никому.
Ограниченный контекст (bounded context) — ответ Эванса на эту проблему, и ответ дерзкий: перестаньте договариваться о едином определении; договоритесь о границе, внутри которой определение одно, и о правилах перевода на границе. Это самая практически ценная идея во всём DDD: тактические блоки можно не применять и жить нормально, а проигнорировать границы контекстов — значит гарантированно получить систему, где изменение цены в каталоге ломает отчёт по НДС.
1. Интуиция: почему одна модель на всех не работает
Мысленный эксперимент со словом «клиент»
Спросите три отдела, что такое клиент.
- Продажи: «тот, кому мы продаём» — сегмент, потенциал сделки, закреплённый менеджер, стадия воронки. Клиент существует ещё до того, как что-то купил.
- Биллинг: «тот, кто платит» — реквизиты, ИНН, кредитный лимит, юрисдикция, баланс. Клиент без платёжных данных — бессмысленная запись.
- Поддержка: «тот, кто обратился» — уровень SLA, каналы связи, история тикетов, язык общения. Здесь клиентом может быть сотрудник клиента, не подписывавший договор.
Ни одно из определений не «правильнее» других: все три верны в своём контексте и несовместимы вне его.
Что происходит, если попытаться объединить
Появляется класс Customer на 80 полей, где 60 всегда null. Дальше по нарастающей:
# Симптом отсутствия границ: один класс обслуживает три реальности
class Customer:
id: int
sales_segment: str | None # продажи
account_manager_id: int | None # продажи
vat_number: str | None # биллинг
credit_limit: Decimal | None # биллинг
sla_tier: str | None # поддержка
preferred_language: str | None # поддержка
def is_valid(self) -> bool:
# А что здесь писать? Валидность зависит от того, КТО спрашивает.
# Для биллинга обязателен vat_number, для поддержки — нет.
# Любой ответ будет неверным для двух из трёх потребителей.
...
Метод is_valid() невозможно написать корректно — и это не проблема кода, а индикатор того,
что в одном классе живут разные инварианты. Формально: если k контекстов делят одну
модель с n правилами каждый, поверхность регрессии при изменении — O(k · n), а число
согласований на одно изменение — O(k) команд. Разрезав модель на k независимых, получаем
O(n) на изменение внутри контекста и O(1) согласований — ценой явной трансляции на
границах. Вся стратегия ниже про то, как сделать цену трансляции меньше цены согласований.
2. Строгое определение
Ограниченный контекст — это явная граница (кодовая, командная и лингвистическая), внутри которой конкретная модель домена определена, непротиворечива и применима; за её пределами модель не имеет силы, а термины требуют перевода.
Разберём определение по частям — каждое слово нагружено.
| Часть определения | Что означает практически | Как проверить |
|---|---|---|
| явная | Граница видна в коде: отдельный модуль/пакет/сервис, отдельная схема БД | Можно показать пальцем на каталог и сказать «вот граница» |
| модель определена | Каждый термин имеет ровно одно значение | Спросите двух разработчиков, что такое «отгрузка» — ответы совпадут |
| непротиворечива | Инварианты внутри не конфликтуют | Нет полей, обязательных для одного сценария и запрещённых для другого |
| не имеет силы вне | Классы контекста не импортируются другими контекстами | Статический анализ импортов зелёный |
| термины требуют перевода | Есть явный слой трансляции на границе | Есть код, который называется маппером/ACL, а не «просто DTO скопировали» |
Контекст ≠ поддомен ≠ микросервис ≠ модуль
Три самых частых путаницы в DDD, и стоят они дорого.
ядровой"] SD2["Поддомен «Расчёты»
поддерживающий"] SD3["Поддомен «Уведомления»
обобщённый"] end subgraph SOLUTION["Пространство решения (что мы построили)"] BC1["Контекст «Сделки»"] BC2["Контекст «Ценообразование»"] BC3["Контекст «Биллинг»"] BC4["Покупное SaaS-решение"] end SD1 --> BC1 SD1 --> BC2 SD2 --> BC3 SD3 --> BC4 BC1 -.->|"деплой: 1 сервис"| D1["orders-service"] BC2 -.->|"деплой: тот же сервис"| D1 BC3 -.->|"деплой: отдельный"| D2["billing-service"]
Ключевые тезисы, видные из схемы:
- Поддомен — из пространства задачи, контекст — из пространства решения. Поддомен дан бизнесом, контекст вы проектируете. Отображение 1:1 — хорошая цель, но 1:N (поддомен разрезан на два контекста из-за разных темпов изменений) встречается постоянно.
- Контекст ≠ единица деплоя. Два контекста могут жить в одном процессе как модули («модульный монолит»), и это часто правильнее двух сервисов. Обратное — один контекст, размазанный по трём сервисам, — почти всегда ошибка: транзакционная граница агрегата окажется распределённой.
- Контекст — единица владения. Одна команда может владеть несколькими контекстами; контекст с двумя владельцами — источник вечного конфликта (либо сознательное Partnership).
Правило: микросервис — способ деплоя контекста, а не сам контекст. Разрезав систему на сервисы, не определив контексты, вы получаете распределённый монолит — сетевые задержки микросервисов при связности монолита (эту тему подробно разбирает трек «Архитектурные паттерны»).
3. Как найти границы: практические эвристики
Границы не выводятся из схемы БД. Они выводятся из наблюдений за языком и за людьми. Вот работающий чек-лист, отсортированный по силе сигнала.
разное в разных сценариях?"} Q1 -->|да| CUT["Сильный сигнал:
граница проходит здесь"] Q1 -->|нет| Q2{"Разные люди принимают
решения об этих правилах?"} Q2 -->|да| CUT Q2 -->|нет| Q3{"Разный темп изменений?
раз в неделю vs раз в год"} Q3 -->|да| CUT Q3 -->|нет| Q4{"Разные требования к данным?
consistency vs availability"} Q4 -->|да| CUT Q4 -->|нет| Q5{"Нужна ли отдельная
транзакционная граница?"} Q5 -->|да| CUT Q5 -->|нет| KEEP["Оставить в одном контексте:
граница дороже пользы"] CUT --> COST{"Сможем ли обслуживать
трансляцию на границе?"} COST -->|да| DONE["Отдельный контекст"] COST -->|нет, команда мала| KEEP2["Модуль внутри контекста,
граница на будущее"]
Расшифровка сигналов:
- Лингвистическая двусмысленность (сильнейший сигнал). Как только вы слышите «ну это
смотря какой заказ» или видите в коде
OrderType,is_internal,mode='B2B'— там граница. Атрибут-переключатель, меняющий смысл половины полей, — это два контекста, склеенных скотчем. - Разные владельцы решений. Правила скидок утверждает коммерческий директор, правила налогов — главбух, и они никогда не встречаются: это два контекста. Закон Конвея работает в обе стороны — границы кода тяготеют к границам коммуникации (Conway, 1968; «Inverse Conway Maneuver» у Skelton & Pais).
- Разный темп изменений. Промо-акции переписывают каждый спринт, бухучёт — раз в несколько лет. Склеив их, вы обрекаете бухгалтерский код на еженедельную регрессию.
- Разные нефункциональные требования. Корзина должна быть доступна всегда и может быть слегка неконсистентной; проводка по счёту — наоборот. Разные точки на CAP-компромиссе плохо уживаются в одной модели.
- Отдельная транзакционная граница. Атомарная операция целиком лежит внутри одного контекста — это ограничение снизу: контекст не может быть меньше самой крупной нужной ему транзакции.
Обратный тест: когда границу проводить НЕ надо
- Контекст из одного CRUD без собственных правил — это не контекст, а таблица; оставьте модулем.
- Если между «контекстами» нужны десятки синхронных вызовов на один сценарий, граница проведена поперёк естественного шва — сшивайте обратно.
- Если команда из трёх человек владеет семью контекстами и семью базами, вы платите за границы больше, чем получаете. Правило Ньюмана из «Building Microservices»: начинайте с модульного монолита и режьте только по доказанным швам.
4. Карта контекстов: девять паттернов отношений
Карта контекстов (context map) — это не диаграмма развёртывания и не схема API. Это карта политических и организационных отношений между командами, выраженная через код. Главный вопрос каждой связи: кто вынужден подстраиваться, когда другая сторона меняется?
Базовая нотация: U (upstream) — тот, кто влияет; D (downstream) — тот, кто подстраивается. Стрелка идёт от U к D.
Разберём каждый — с критерием применимости и ценой.
4.1 Partnership (партнёрство)
Две команды взлетают и падают вместе: провал интеграции означает провал обеих. Координация релизов, общие интеграционные тесты, совместное планирование. Когда: два ядровых контекста, которые физически нельзя развивать независимо (заказы и доставка в логистике). Цена: связность процессов — каждая крупная фича требует согласования двух бэклогов. Симптом деградации: партнёрство «на бумаге», когда одна команда фактически диктует — переименуйте в Customer–Supplier и живите честно.
4.2 Shared Kernel (общее ядро)
Явно выделенное подмножество модели (типы, схема, код), разделяемое двумя и более контекстами;
менять можно только по согласию всех владельцев, с прогоном тестов всех потребителей.
Что кладут: Money, Currency, TimeRange, идентификаторы, базовые ошибки — обобщённые
примитивы без бизнес-правил конкретного контекста. Чего класть нельзя: сущности с
поведением, статусы бизнес-процессов, DTO API. Цена: SK — точка максимальной связности на
карте. Если он растёт каждый спринт, вы вернулись к общей модели.
4.3 Customer–Supplier (заказчик–поставщик)
Upstream признаёт потребности downstream и берёт их в свой бэклог: есть переговоры, приоритеты, сроки; downstream участвует в приёмочном тестировании upstream. Когда: обе команды внутри одной организации и есть кто-то, способный разрешить конфликт приоритетов. Цена: upstream тратит часть ёмкости на чужие требования.
4.4 Conformist (конформист)
Downstream принимает модель upstream как свою, без трансляции: отказ от собственной модели в обмен на нулевую цену интеграции. Когда: модель upstream приемлема, домен для вас неядровой, ресурсов на ACL нет (интеграция с платёжным провайдером в маленьком сервисе). Цена: любое изменение upstream протекает прямо в вашу модель. Никогда не применяйте Conformist в ядровом поддомене — это отдаёт наружу главный актив компании.
4.5 Anticorruption Layer (ACL, предохранительный слой)
Downstream строит слой перевода: чужая модель конвертируется в термины собственного языка, и внутрь контекста чужие типы не попадают вообще. Когда: чужая модель плоха, легаси, нестабильна или просто чужая — а ваш контекст ядровой. Цена: код маппинга и лишний слой при отладке. Выгода: изменение upstream локализуется в одном файле. ACL — самый частый и самый недооценённый паттерн; практически всё постепенное вытеснение легаси строится на нём (Fowler, «StranglerFigApplication»).
4.6 Open Host Service + 4.7 Published Language
OHS: upstream публикует общий версионированный протокол для всех потребителей вместо частного интерфейса на каждого. Применяют, когда потребителей больше 2–3; цена — обязательство держать обратную совместимость и депрекейт-политику.
PL: хорошо описанный формат обмена — JSON Schema, Avro/Protobuf, OpenAPI или отраслевой
стандарт (ISO 20022 в финтехе, HL7/FHIR в медицине). Обычно идёт в паре с OHS. Ключевое: PL —
не внутренняя модель, выставленная наружу, а отдельный, намеренно более бедный и стабильный
контракт. Если @Entity сериализуется прямо в ответ API — у вас не Published Language,
а утечка модели.
4.8 Separate Ways + 4.9 Big Ball of Mud
Separate Ways: интеграции нет, каждый контекст решает задачу сам, даже ценой дублирования. Когда интеграция дороже копии: поддержке нужны только имя и email — проще хранить копию, чем звать сервис клиентов на каждый тикет. Цена — расхождение данных; осознанное дублирование не ошибка, а плата за автономию.
Big Ball of Mud: отсутствие границ. Отметьте эту зону на карте честно — как область, куда нельзя пускать новую модель (Foote & Yoder). Стратегия: обнести ACL и постепенно вытеснять.
Сводная таблица выбора
| Ситуация | Паттерн | Почему |
|---|---|---|
| Мы ядровой контекст, upstream — легаси | ACL | Защитить главный актив |
| Мы неядровой, upstream хорош и стабилен | Conformist | Не платить за трансляцию |
| У нас 5+ потребителей | OHS + Published Language | Один контракт вместо пяти |
| Обе команды в одной компании, есть переговоры | Customer–Supplier | Приоритеты решаемы |
| Два ядровых контекста, судьба общая | Partnership | Иначе рассинхрон релизов |
| Нужны общие примитивы (Money, Id) | Shared Kernel (маленький) | Дешевле копипасты |
| Пересечение мизерное | Separate Ways | Дублирование дешевле связи |
5. Код: две модели и ACL между ними
Покажем полный вертикальный срез. Контекст Заказы (ядровой, downstream) интегрируется с контекстом Каталог (upstream, OHS + Published Language).
5.1 Опубликованный язык upstream-контекста
# catalog/published_language.py — КОНТРАКТ, а не внутренняя модель каталога.
# Меняется только с увеличением версии; внутри каталога модель богаче и другая.
from dataclasses import dataclass
@dataclass(frozen=True)
class ProductSnapshotV1:
"""Минимально необходимая проекция товара для внешних потребителей."""
sku: str
display_name: str
price_minor_units: int # цена в минорных единицах — без float
currency_code: str # ISO 4217
is_orderable: bool # каталог сам решает, что значит «можно заказать»
tax_category: str # 'standard' | 'reduced' | 'zero'
schema_version: str = "catalog.product.v1"
Обратите внимание: is_orderable — это решение каталога, выраженное одним булевым полем.
Каталог не выставляет наружу stock_status, is_archived, visibility_rules, из которых
это решение складывается. Иначе каждый потребитель начнёт вычислять «заказуемость» сам,
и правило размножится по системе — ровно та болезнь, которую лечит DDD.
5.2 Модель downstream-контекста
# orders/domain/model.py — собственный язык контекста «Заказы».
# Здесь нет ни слова «product», ни «sku» из чужой терминологии — только наши термины.
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
@dataclass(frozen=True)
class OrderableItem:
"""То, что заказы знают о товаре. Ровно столько, сколько нужно для их правил."""
item_code: str # наш термин вместо чужого 'sku'
title: str
unit_price: Money
vat_rate: Decimal # ставка, а не чужая строковая 'tax_category'
class Order:
def __init__(self, order_id: str, currency: str) -> None:
self._id, self._currency = order_id, currency
self._lines: list[tuple[OrderableItem, int]] = []
def add_line(self, item: OrderableItem, quantity: int) -> None:
# Инвариант принадлежит контексту «Заказы» и проверяется здесь, а не в каталоге.
if quantity <= 0:
raise ValueError("Количество должно быть положительным")
if item.unit_price.currency != self._currency:
raise ValueError("Валюта позиции не совпадает с валютой заказа")
self._lines.append((item, quantity))
5.3 Собственно ACL
# orders/infrastructure/catalog_acl.py
# Единственное место во всём контексте «Заказы», которое знает про формат каталога.
from decimal import Decimal
from typing import Protocol
from catalog.published_language import ProductSnapshotV1
from orders.domain.model import Money, OrderableItem
class ItemNotOrderable(Exception):
"""Доменная ошибка НАШЕГО контекста, а не чужой HTTP-код."""
class CatalogPort(Protocol):
"""Порт описан в терминах контекста «Заказы» — инверсия зависимости."""
def fetch_orderable_item(self, item_code: str) -> OrderableItem: ...
# Таблица трансляции — знание, которое иначе расползлось бы по коду
_VAT_BY_CATEGORY = {"standard": Decimal("0.20"),
"reduced": Decimal("0.10"),
"zero": Decimal("0.00")}
class HttpCatalogAdapter:
"""Реализация порта: HTTP + трансляция чужой модели в нашу."""
def __init__(self, client) -> None:
self._client = client
def fetch_orderable_item(self, item_code: str) -> OrderableItem:
raw = self._client.get(f"/api/v1/products/{item_code}")
return self._translate(ProductSnapshotV1(**raw))
def _translate(self, s: ProductSnapshotV1) -> OrderableItem:
# 1. Неизвестная версия схемы — авария интеграции, а не «вдруг распарсится».
if s.schema_version != "catalog.product.v1":
raise RuntimeError(f"Неподдерживаемая версия схемы: {s.schema_version}")
# 2. Чужое решение переводим в НАШУ доменную ошибку.
if not s.is_orderable:
raise ItemNotOrderable(f"Товар {s.sku} недоступен для заказа")
# 3. Чужая строковая категория → наша ставка. Незнакомая — падаем громко,
# а не подставляем 0: тихий ноль даст финансовую ошибку через квартал.
try:
vat = _VAT_BY_CATEGORY[s.tax_category]
except KeyError as exc:
raise RuntimeError(f"Неизвестная налоговая категория: {s.tax_category}") from exc
# 4. Минорные единицы → Money, чужие имена полей → наши.
return OrderableItem(
item_code=s.sku,
title=s.display_name,
unit_price=Money(Decimal(s.price_minor_units) / 100, s.currency_code),
vat_rate=vat,
)
Что даёт конструкция: контекст «Заказы» тестируется без каталога (подставляем фейковый
CatalogPort); переход каталога на v2 меняет один файл, домен не трогается; незнакомые
значения приводят к громкому отказу, а не к тихой порче данных.
5.4 Поток вызова и деградация
Про fallback: кэш: выбор между «упасть» и «взять устаревшую копию» — доменное решение, а не
техническое. Для цены устаревание на 5 минут недопустимо (продадим по старой цене), для
названия — нормально. Поэтому решение принимается в ACL потребителя, знающего цену ошибки,
а не в общем HTTP-клиенте.
6. Как контексты живут и мигрируют
Границы не проводятся один раз. Полезно смотреть на контекст как на объект с жизненным циклом.
слишком болтливая связь ОтдельнаяСхема --> Слияние: транзакции всё время распределённые Слияние --> [*] Стабильный --> Разделение: внутри выросли два языка Разделение --> Модуль Стабильный --> Вывод: поддомен закрыт
или заменён покупным решением Вывод --> [*]
Три наблюдения по этому графу:
- Стрелка
Модуль → Слияниеобязана существовать. Гипотеза о границе бывает неверной, и откат — нормальный исход. Откатить модуль дёшево, сервис — дорого. Отсюда правило: сначала модуль, потом сервис, никогда наоборот. - Переход
ОтдельнаяСхема— главный водораздел. Пока два контекста делаютJOINпо таблицам друг друга, границы нет, что бы ни было написано в архитектурном документе. Общая база — это Shared Kernel размером со всю модель, только необъявленный. Стабильный → Разделениеслучается, когда внутри появляется прилагательное: «внутренний заказ», «подписочный заказ». Прилагательное перед ключевым существительным — ранний признак того, что зреет новый контекст.
Порядок разрезания монолита
Рецепт, проверенный на многих миграциях (см. Newman, «Monolith to Microservices»):
- Логическая граница: пакет
orders/, публичныйorders/api.py, остальное приватно. - Запрет импортов в CI — без автоматики граница разрушится за пару спринтов.
- Разделение данных: убрать
JOINчерез границу, заменив вызовом API или репликой. Самый трудоёмкий шаг — обычно 70% усилий миграции. - Разделение транзакций: вместо одной транзакции — сага или eventual consistency (см. доменные события и интеграцию).
- Только теперь — отдельный процесс, если он даёт конкретное: независимый релизный цикл, отдельное масштабирование, изоляцию отказов.
Если шаги 1–4 сделаны, шаг 5 занимает дни. Если начать с шага 5, шаги 1–4 не будут сделаны никогда — их придётся делать по сети и под нагрузкой.
7. Как границы удерживают в проде
Граница, которую не проверяет автоматика, не существует. Ниже — минимальный набор.
7.1 Статическая проверка импортов
Для Python — import-linter:
# setup.cfg (или .importlinter)
[importlinter]
root_packages = orders catalog billing
[importlinter:contract:1]
name = Контексты не импортируют внутренности друг друга
type = forbidden
source_modules = orders.domain
orders.application
forbidden_modules = catalog
billing
# исключение ровно одно: ACL имеет право знать про опубликованный язык
ignore_imports = orders.infrastructure.catalog_acl -> catalog.published_language
[importlinter:contract:2]
name = Домен не зависит от инфраструктуры
type = forbidden
source_modules = orders.domain
forbidden_modules = orders.infrastructure
sqlalchemy
requests
Аналоги в других экосистемах: ArchUnit (Java/Kotlin),
NetArchTest и .editorconfig-правила (C#),
dependency-cruiser (TypeScript),
internal/-пакеты и go vet в Go — про них подробнее в
треке Go.
7.2 Контрактные тесты вместо сквозных
Сквозной тест «поднимем каталог, заказы и биллинг и прокликаем сценарий» долгий, хрупкий и не сообщает, кто сломал контракт. Контрактный тест проверяет только границу:
# tests/contract/test_catalog_acl.py
import pytest
from decimal import Decimal
from orders.infrastructure.catalog_acl import HttpCatalogAdapter, ItemNotOrderable
class FakeClient:
"""Отдаёт ровно тот JSON, который зафиксирован в контракте каталога."""
def __init__(self, payload): self._payload = payload
def get(self, _url): return self._payload
BASE = {"sku": "SKU-42", "display_name": "Кофе 1 кг", "price_minor_units": 129900,
"currency_code": "RUB", "is_orderable": True, "tax_category": "reduced",
"schema_version": "catalog.product.v1"}
def test_перевод_в_модель_заказов():
item = HttpCatalogAdapter(FakeClient(BASE)).fetch_orderable_item("SKU-42")
assert item.unit_price.amount == Decimal("1299.00") # минорные единицы переведены
assert item.vat_rate == Decimal("0.10") # категория → ставка
def test_недоступный_товар_даёт_нашу_доменную_ошибку():
with pytest.raises(ItemNotOrderable):
client = FakeClient(BASE | {"is_orderable": False})
HttpCatalogAdapter(client).fetch_orderable_item("SKU-42")
@pytest.mark.parametrize("broken", [
{"schema_version": "catalog.product.v2"}, # upstream сменил версию
{"tax_category": "luxury"}, # upstream добавил категорию
])
def test_изменение_контракта_ломает_сборку_а_не_прод(broken):
with pytest.raises(RuntimeError):
HttpCatalogAdapter(FakeClient(BASE | broken)).fetch_orderable_item("SKU-42")
В проде это дополняется consumer-driven contract testing (Pact, Fowler о CDC): потребитель публикует свои ожидания, а CI поставщика падает, если новая версия их нарушает. Это превращает связь «U → D» с карты контекстов в исполняемую проверку.
7.3 Карта контекстов как живой артефакт
Держите карту в репозитории рядом с кодом (mermaid в docs/context-map.md), а не в презентации
трёхлетней давности. На каждой связи подписывайте паттерн и владельца:
Каталог --[OHS/PL, v1, owner: team-catalog]--> Заказы. Ревизия — раз в квартал и обязательно
перед крупным разрезанием монолита. Если нравится формальность, есть
Context Mapper — DSL для контекст-мапов с генерацией диаграмм.
8. Типичные ошибки
| Ошибка | Как выглядит | Чем лечится |
|---|---|---|
| Контекст = микросервис | Режут по сущностям: user-service, order-service, address-service |
Резать по языку и правилам, а не по таблицам |
| Общая БД у «разных» контекстов | JOIN orders o, billing_invoices i |
Схема на контекст; кросс-границы только через API/события |
| Каноническая модель на всю компанию | Проект «единый справочник клиента» длиной в два года | Published Language вместо канонической модели: контракт обмена, а не общая правда |
| ACL, который ничего не переводит | Маппер копирует поля один в один | Если перевода нет, вы Conformist — назовите честно и не платите за слой |
| Shared Kernel как свалка | В общей библиотеке common лежат статусы заказа |
В SK только примитивы без бизнес-правил |
| Conformist в ядре | Ядровая модель повторяет схему стороннего SaaS | ACL: ядро обязано говорить своим языком |
| Границы без автоматики | В документе граница есть, в коде импортируют напрямую | import-linter/ArchUnit в CI, падающая сборка |
| Слишком много контекстов | 3 разработчика, 9 репозиториев, 9 баз | Слить обратно; граница — это модуль до тех пор, пока не доказана |
| Синхронная болтливость через границу | 40 HTTP-вызовов на один сценарий | Граница проведена не по шву: либо сшить, либо перейти на события/реплику данных |
| Дублирование считают ошибкой | Требуют «единственный источник правды» для каждого поля | Осознанная копия read-only данных — законная плата за автономию |
Про то, как эти ошибки складываются в общий провал внедрения, — отдельная статья трека: типичные ошибки внедрения DDD.
9. Мини-итог
- Ограниченный контекст — граница, внутри которой термин означает ровно одно: граница лингвистическая, ставшая границей кода и владения.
- Один термин в разных контекстах обязан означать разное; общее у моделей — только идентификатор и явный контракт обмена.
- Поддомен — из пространства задачи, контекст — из пространства решения, микросервис — способ деплоя контекста. Путать их дорого.
- Границы ищут по языку, владельцам решений, темпу изменений, требованиям к данным, транзакциям — именно в этом порядке силы сигнала.
- Карта контекстов отвечает на вопрос, кто кому диктует форму модели. Защита ядра — ACL; забота о потребителях — OHS + Published Language; отказ от интеграции (Separate Ways) — тоже валидное решение.
- Порядок миграции: модуль → запрет импортов → своя схема → свои транзакции → свой сервис. Обратный порядок даёт распределённый монолит.
- Граница без автоматической проверки исчезает за 2–3 спринта.
Источники
- Eric Evans. Domain-Driven Design, гл. 14 «Maintaining Model Integrity» — первоисточник; плюс бесплатный DDD Reference с точными определениями всех паттернов карты контекстов.
- Vaughn Vernon. Implementing Domain-Driven Design, гл. 2–3 — практика контекстов и карт.
- Vlad Khononov. Learning Domain-Driven Design (O’Reilly, 2021) — современный разбор выбора паттернов интеграции.
- Martin Fowler. BoundedContext, StranglerFigApplication, ConsumerDrivenContracts.
- Sam Newman. Building Microservices, 2nd ed., Monolith to Microservices; Skelton & Pais, Team Topologies.
- Foote & Yoder, Big Ball of Mud; Context Mapper — DSL для карт контекстов.
Что дальше
Границы проведены, отношения между контекстами описаны. Дальше — заглянуть внутрь одного контекста и посмотреть, из чего собирается модель: какие объекты имеют идентичность, какие определяются только значением, где проходят транзакционные границы и куда девать логику, которая не принадлежит ни одному объекту.
Тактические блоки: сущности, объекты-значения, агрегаты, сервисы