$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогПроектируем домен для API: bounded contexts и явные границы ответственности

Проектируем домен для API: bounded contexts и явные границы ответственности

$ sudo teach IT
·22 июля 2026 г.·10 мин·49
Проектируем домен для API: bounded contexts и явные границы ответственности

Соберём доменную модель так, чтобы API отражало смысл, а не “табличную” структуру. Рассмотрим границы контекстов, модели команд/запросов и избегание утечек.

Содержание
Почему API часто «табличное», и к чему это приводитbounded contexts: основной принцип проектированияМодель границ на практикеПроектируем домен, чтобы API говорил смыслом, а не таблицамиПочему разделение команд и запросов полезно именно для границСтратегия: выделяем контексты на уровне APIПример bounded contexts и типовые границы ответственностиИзбегаем утечек: что именно запрещено переносить через границыУтечки терминовУтечки инвариантовУтечки структуры данныхМодели команд: как формировать API действияПринципы хороших командПример: модель команды “Списать”Доменные ошибки как часть контрактаМодели запросов: проекция данных без разрушения доменаПравильный контракт запроса — это проекцияПример: представление заказаИнтеграция контекстов: события, анти-утечки и адаптацияТиповой механизм: доменные событияПример события (контекст платежей → контекст заказов)Где адаптеры уместныСинхронные запросы между контекстами: когда можно, когда нельзяРиски синхронной интеграцииКомпромисс: “контракт факта”, а не “контракт модели”Практика: как не запутаться в API-структуреО чём помнить при проектировании маршрутовВерсионирование и стабильностьМинимальный референс-процесс: как организовать работу над доменом и APIШаг 1. Выпишите словарь доменаШаг 2. Выделите bounded contexts и границыШаг 3. Проектируйте контракты от смыслов, а не от таблицШаг 4. Зафиксируйте “запреты” на утечкиШаг 5. Добавьте адаптацию на границеТипичные ошибки при проектировании bounded contexts и APIОшибка 1: “Всё — один контекст”Ошибка 2: “Контекст есть на диаграмме, но нет в коде”Ошибка 3: Использование единого DTO между командами и запросами

API почти всегда выглядит «технически», но его качество определяется не фреймворком и не версионированием на уровне URL. В долгоживущих системах API становится интерфейсом домена: он должен говорить на языке бизнеса и удерживать модель от деградации в «табличную магию». Именно здесь доменное проектирование, bounded contexts и дисциплина границ ответственности дают измеримый эффект — меньше утечек, меньше случайных зависимостей, понятнее эволюция.

В этой статье разберём практический подход к проектированию домена для API: как выделять bounded contexts, как формировать модели команд/запросов и как предотвращать утечки терминов и инвариантов между частями системы. Будем говорить достаточно конкретно, чтобы этим можно было пользоваться при проектировании нового сервиса или при исправлении текущих проблем.


Почему API часто «табличное», и к чему это приводит

Типичный антипример выглядит так: у нас есть таблицы users, orders, payments, мы поднимаем CRUD-эндпоинты и начинаем встраивать в контракт API поля, которые соответствуют колонкам.

Проблемы возникают постепенно:

  1. API начинает отражать структуру хранения, а не смысл.
    Если завтра изменится способ хранения (например, нормализация, шардинг, кэш, архивирование), API начинает «нести» это изменение на себе.

  2. Инварианты расползаются по слоям.
    Проверки вроде “заказ можно отменить только если …” начинают жить в контроллерах, сервисах и хранимых процедурах одновременно. Домен перестаёт быть единственным источником истины.

  3. Утечки модели между контекстами.
    Термины становятся размытыми. Например, в одном месте status — это жизненный цикл заказа, а в другом — состояние процесса оплаты. Оба контекста используют одно и то же поле в DTO, потому что «так проще».

  4. Сложность изменения растёт нелинейно.
    Любая правка контракта превращается в расследование: где именно нужно менять поле, как обновить фронт, как мигрировать данные, какие сценарии ломаются.

Чтобы этого избежать, API стоит проектировать как внешнюю проекцию конкретного bounded context, а не как «витрину базы данных».


bounded contexts: основной принцип проектирования

Bounded context — это границы, в которых определены:

  • терминология (какие слова и что они означают),
  • инварианты (какие правила нельзя нарушить),
  • логика поведения (какие действия возможны),
  • контракт взаимодействия (какой API говорит именно этот контекст).

Ключевой момент: в разных bounded contexts одна и та же сущность может иметь разное значение. Например, “клиент” в контексте CRM — одно, а “покупатель” в контексте заказов — другое. Даже если внешне это выглядит похоже, инварианты могут отличаться.

Модель границ на практике

В реальном проекте выделить bounded context помогают ответы на вопросы:

1) Где находится единый смысл?

Если вы произносите слово “заказ” и в разных командах вкладывается разный смысл — вероятно, это несколько контекстов.
Если смысл единый и правила совпадают — это кандидат на один bounded context.

2) Какие правила защищают модель?

В каждом bounded context должны быть свои доменные правила.
Если правила зависят от внешних деталей другого контекста (например, статуса платежа), значит границы размываются — либо нужен согласованный контракт, либо события и адаптация.

3) Какая команда отвечает за изменения?

Bounded context — это ещё и организационная граница. Если одно и то же изменение вынуждает координировать 5 команд и перепроверять 10 модулей, возможно, у вас не контекст, а “конвейер в монолите”.


Проектируем домен, чтобы API говорил смыслом, а не таблицами

Когда bounded contexts определены, возникает вопрос: как связать домен и API?

Один из самых практичных подходов — разделить:

  • модель команд (commands) / действий — что пользователь/сервис намеревается сделать;
  • модель запросов (queries) / чтения — какие данные нужно получить.

Это помогает держать доменные правила и инварианты там, где они принадлежат контексту, а DTO для чтения не превращаются в “карманные объекты” для записи.

Почему разделение команд и запросов полезно именно для границ

Если вы смешиваете “чтение” и “запись” в один объект (например, один DTO используется для POST и GET и содержит все поля), то вы начинаете:

  • экспортировать внутренние поля наружу,
  • терять контроль над тем, какие действия разрешены,
  • случайно привязываться к структуре хранения.

Разделяя модели, проще удерживать границы: команды отражают намерение и инварианты, запросы — проекцию данных для потребителя.


Стратегия: выделяем контексты на уровне API

Рассмотрим типичный домен e-commerce, где часто возникают утечки:

  • Checkout / Заказы: формирование и жизненный цикл заказа.
  • Payments / Платежи: авторизация, списание, статусы платежных операций.
  • Catalog / Каталог (или Pricing): доступность и цены.
  • Identity / Пользователи (опционально): учетные записи.

Пример bounded contexts и типовые границы ответственности

1) Контекст “Заказы”

Ответственность:

  • создавать заказ,
  • управлять его жизненным циклом (draft → submitted → confirmed → cancelled),
  • обеспечивать инварианты заказа (например, нельзя подтвердить заказ без подтвержденного состава).

Что API здесь обычно делает:

  • POST /orders — создать заказ (команда)
  • POST /orders/{id}/confirm — подтвердить (команда)
  • POST /orders/{id}/cancel — отменить (команда)
  • GET /orders/{id} — получить представление заказа (запрос)

Контракт не обязан раскрывать детали расчетов или платежей. Контекст заказа может иметь “ссылки” на внешние операции, но не обязан копировать их правила.

2) Контекст “Платежи”

Ответственность:

  • принимать платежные команды (authorize/capture/refund),
  • хранить статусы платежных операций,
  • обеспечивать инварианты платежного процесса (например, refund возможен только для захваченных платежей).

Контекст платежей может иметь связь с заказами через:

  • события (“PaymentAuthorized”, “PaymentCaptured”),
  • идентификаторы внешнего агрегата (например, orderId как ссылку).

Критично: правила статусов платежа принадлежат контексту платежей. Контекст заказов не должен “притворяться” платежным движком.


Избегаем утечек: что именно запрещено переносить через границы

Утечка — это не только «неправильные поля в JSON». Это перенос смыслов и инвариантов.

Утечки терминов

Пример:

  • в контексте заказов status=PAID означает “заказ оплачен”,
  • в контексте платежей status=PAID означает “операция платежа завершилась успешно”.

В итоге появляется соблазн использовать один и тот же status в API заказов, ориентируясь на статусы платежей. Это ломает независимость и вызывает ошибки при расхождении моделей.

Правильнее: держать терминологию контекстной. В API заказа можно иметь поле вроде:

  • paymentState: "pending" | "authorized" | "settled",
    и оно должно быть проекцией состояния, а не “скопированным платежным статусом”.

Утечки инвариантов

Если контроллер заказа проверяет условия, относящиеся к контексту платежей (“отменить заказ можно только если платеж еще не захвачен”), значит инварианты разнесены между контекстами.

Правильнее: контекст заказа должен иметь собственные правила, а платежи — предоставлять факты через события/запросы. Даже если бизнес-логика “взаимосвязана”, она не обязана быть “размазана” по кодовой базе одного контекста.

Утечки структуры данных

Если DTO для команды содержит поля “как в таблице платежной операции”, то вы фактически публикуете внутреннее устройство контекста платежей.

Правильнее: команды должны быть минимально достаточными и выражать намерение.


Модели команд: как формировать API действия

Команда — это намерение сделать действие. Важно: команда не обязана отражать агрегат “1 к 1”. Она обязана отражать ограничения и требования, необходимые доменной логике.

Принципы хороших команд

1) Команда должна быть адресована конкретному контексту

Если вы пишете POST /payments/capture, это команда контексту платежей, а не универсальный механизм на все случаи жизни.

2) Команда должна содержать минимальный набор данных

Если контекст может вычислить сумму сам по своим данным — не передавайте ее извне без необходимости.

3) В команде должны отсутствовать внутренние “технические” поля

Например, paymentRowId, processorTxnId — это детали хранения/интеграции.

Пример: модель команды “Списать”

Допустим, контекст платежей должен списать авторизованный платеж.

code
POST /payments/{paymentId}/capture
{
  "amount": 1200,
  "currency": "RUB",
  "idempotencyKey": "c2f1b6b9-1e4d-4a7b-9a5a-1b3f2c4d5e6f"
}

Что здесь важно:

  • idempotencyKey — полезен для сетевых дублей,
  • сумма может быть частичной (если доменная логика поддерживает частичное списание),
  • paymentId — ссылка на сущность контекста платежей.

Контракт не обязан содержать “status до операции” — домен знает текущее состояние.

Доменные ошибки как часть контракта

Ошибки команды должны быть контекстными и предсказуемыми. Например:

  • “PAYMENT_NOT_AUTHORIZED”
  • “CAPTURE_ALREADY_DONE”
  • “AMOUNT_EXCEEDS_AUTHORIZED”

Это даёт потребителю API возможность корректно реагировать, не залезая в детали реализации.


Модели запросов: проекция данных без разрушения домена

Запросы обычно проще: они возвращают данные. Но и здесь границы важны: представление должно соответствовать потребностям клиента, а не внутренним сущностям.

Правильный контракт запроса — это проекция

Например, запрос GET /orders/{id} может вернуть:

  • orderId
  • state (жизненный цикл заказа)
  • items (состав)
  • payment (проекция состояния оплаты)
  • total (если проекция нужна)

Но не стоит возвращать “сырой внутренний платежный статус”, если его смысл другой.

Пример: представление заказа

code
GET /orders/ORD-100500
{
  "orderId": "ORD-100500",
  "state": "confirmed",
  "items": [
    { "sku": "T-SHIRT-XL", "qty": 1, "price": 1200 }
  ],
  "payment": {
    "state": "authorized",
    "lastUpdatedAt": "2026-07-22T10:15:30Z"
  },
  "total": 1200
}

Обратите внимание: payment.state здесь — именно проекция, которая принадлежит доменному языку заказа (а не платежного контекста).


Интеграция контекстов: события, анти-утечки и адаптация

Когда bounded contexts разделены, они всё равно должны взаимодействовать. Основная проблема — как передавать изменения, не ломая модель.

Типовой механизм: доменные события

Контекст платежей публикует события:

  • PaymentAuthorized
  • PaymentCaptured
  • PaymentFailed

Контекст заказов подписывается и обновляет свои проекции/состояния.

Проблема возникает, когда событие несёт слишком много “внутреннего”. Поэтому важен дизайн событий.

Принципы событий

  1. Событие описывает факт произошедшего, а не внутреннюю процедуру.
    Например, “PaymentCaptured” — факт. “PaymentRowUpdated” — внутреннее.

  2. Событие содержит контекстные данные, нужные получателю.
    Контекст заказа может не знать “процессор”, но ему нужен amount, currency (если это важно для инвариантов) и ссылка на orderId.

  3. Не публикуйте внутренние структуры и таблицы.

Пример события (контекст платежей → контекст заказов)

code
{
  "type": "PaymentCaptured",
  "occurredAt": "2026-07-22T10:15:30Z",
  "paymentId": "PAY-9001",
  "orderId": "ORD-100500",
  "amount": 1200,
  "currency": "RUB",
  "reference": "psp_ref_12345"
}

Контекст заказа по этому факту может перевести payment.state в settled или выполнить действия (например, подтвердить заказ, если это правило домена).

Где адаптеры уместны

Иногда интеграция требует маппинга: один контекст говорит на своём языке, другой ожидает другой смысл. Адаптация делается на границе:

  • преобразуем события в модель получателя,
  • переводим терминологию,
  • применяем правила корректного соответствия.

Синхронные запросы между контекстами: когда можно, когда нельзя

События — хороший выбор для асинхронного согласования, но иногда нужна синхронная проверка (например, “можно ли выполнить действие прямо сейчас?”).

Синхронные вызовы между bounded contexts должны быть редкими и ограниченными.

Риски синхронной интеграции

  1. Жёсткая связность.
    Модель одного контекста вынуждена “жить” в ответах другого.

  2. Дублирование инвариантов.
    Логика начинает проверять внешние условия через API, вместо того чтобы опираться на свои правила и события.

Компромисс: “контракт факта”, а не “контракт модели”

Если нужно убедиться в факте, лучше проектировать endpoint как проверку факта:

  • GET /orders/{id}/payment-state — возвращает проекцию,
  • или POST /orders/{id}/validate — команда в контекст заказа, а не запрос к платежам из контроллера.

Практика: как не запутаться в API-структуре

Даже при корректном домене остаётся риск запутать внешнего потребителя: endpoints превращаются в набор “ручек”, где имена не отражают смысл.

О чём помнить при проектировании маршрутов

Не делайте ресурсы “табличными”

Плохо:

  • GET /payment_transactions/{id} — если потребителю нужен смысл “оплата заказа”, а не таблица транзакций.

Лучше:

  • GET /orders/{id} с проекцией оплаты,
  • или GET /payments/{paymentId} в контексте платежей.

Именуйте действия как намерение

Плохо:

  • POST /orders/{id}/updatePaymentStatus

Лучше:

  • POST /orders/{id}/confirm
  • POST /orders/{id}/cancel

Версионирование и стабильность

Bounded context помогает стабильности: контракт внешнего API связан с контекстом и его языке. Если меняется внутренняя реализация, контракт может не требовать изменений. Версионирование — следствие реальной смены смысла, а не следствие смены таблиц.


Минимальный референс-процесс: как организовать работу над доменом и API

Ниже — практический workflow, который хорошо работает в команде:

Шаг 1. Выпишите словарь домена

  • Какие термины используются?
  • Кто за них отвечает?
  • Что именно означает каждое понятие (инварианты)?

Шаг 2. Выделите bounded contexts и границы

Для каждого контекста:

  • определите командные сценарии (что делают),
  • определите запросы (что показывают),
  • определите события (что публикуют и что потребляют).

Шаг 3. Проектируйте контракты от смыслов, а не от таблиц

Сначала — команды (intent), затем — запросы (projection).
DTO для команд и запросов могут быть разными.

Шаг 4. Зафиксируйте “запреты” на утечки

Например:

  • нельзя возвращать платежные статусы как есть в API заказов;
  • нельзя принимать в командах поля, которые являются внутренними идентификаторами хранения.

Шаг 5. Добавьте адаптацию на границе

Маппинг событий → проекции → контекстные модели.


Типичные ошибки при проектировании bounded contexts и API

Ошибка 1: “Всё — один контекст”

Когда команда пытается покрыть весь домен одним набором сущностей и DTO, bounded context отсутствует на практике. API разрастается, а инварианты начинают конкурировать.

Симптомы:

  • команды начинают знать слишком много про другие части системы,
  • DTO содержат “половину базы”,
  • ошибки трудно объяснить доменной логикой.

Ошибка 2: “Контекст есть на диаграмме, но нет в коде”

Название payments и orders в папках ещё не означает bounded context. Если правила, терминология и модели смешиваются, утечки продолжатся.

Симптомы:

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

Ошибка 3: Использование единого DTO между командами и запросами

Это приводит

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Проектируем пагинацию правильно: offset vs cursor и стабильные страницы
проектируем

Проектируем пагинацию правильно: offset vs cursor и стабильные страницы

Разберём, почему offset-пагинация ломается на изменениях данных, а cursor-based даёт стабильность. Рассмотрим форматы курсоров, сортировки, идемпотентность и требования к индексу.

25 июля 2026 г.
700
Как писать эффективные SQL-запросы под доменную модель: меньше JOIN’ов, больше ясности
писать

Как писать эффективные SQL-запросы под доменную модель: меньше JOIN’ов, больше ясности

Разберём технику проектирования запросов от сущностей к данным: фильтрация, агрегации, ограничение полей и предсказуемые результаты. Покажем, как избегать дублирования строк и “случайных” декартовых произведений.

25 июля 2026 г.
630
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему API часто «табличное», и к чему это приводитbounded contexts: основной принцип проектированияМодель границ на практикеПроектируем домен, чтобы API говорил смыслом, а не таблицамиПочему разделение команд и запросов полезно именно для границСтратегия: выделяем контексты на уровне APIПример bounded contexts и типовые границы ответственностиИзбегаем утечек: что именно запрещено переносить через границыУтечки терминовУтечки инвариантовУтечки структуры данныхМодели команд: как формировать API действияПринципы хороших командПример: модель команды “Списать”Доменные ошибки как часть контрактаМодели запросов: проекция данных без разрушения доменаПравильный контракт запроса — это проекцияПример: представление заказаИнтеграция контекстов: события, анти-утечки и адаптацияТиповой механизм: доменные событияПример события (контекст платежей → контекст заказов)Где адаптеры уместныСинхронные запросы между контекстами: когда можно, когда нельзяРиски синхронной интеграцииКомпромисс: “контракт факта”, а не “контракт модели”Практика: как не запутаться в API-структуреО чём помнить при проектировании маршрутовВерсионирование и стабильностьМинимальный референс-процесс: как организовать работу над доменом и APIШаг 1. Выпишите словарь доменаШаг 2. Выделите bounded contexts и границыШаг 3. Проектируйте контракты от смыслов, а не от таблицШаг 4. Зафиксируйте “запреты” на утечкиШаг 5. Добавьте адаптацию на границеТипичные ошибки при проектировании bounded contexts и APIОшибка 1: “Всё — один контекст”Ошибка 2: “Контекст есть на диаграмме, но нет в коде”Ошибка 3: Использование единого DTO между командами и запросами