
В предыдущей статье “Упакуйте свой код правильно” мы рассмотрели различные подходы к организации кода, включая монолитную и многослойную архитектуры, принципы чистой архитектуры, а также концепцию Bounded Context (ограниченного контекста) в Domain-Driven Design (DDD). Мы узнали, что Bounded Context помогает структурировать сложные системы, разделяя их на логические части, каждая из которых имеет собственную модель предметной области с четкими и непротиворечивыми правилами.
Теперь, когда мы понимаем, как выделять и организовывать bounded contexts, пришло время углубиться в следующий важный аспект DDD — коммуникацию между ними. В реальных системах bounded contexts редко существуют изолированно. Для реализации бизнес-процессов и обеспечения целостности системы необходимо организовать эффективное взаимодействие между ними. В этой статье мы рассмотрим, как bounded contexts могут общаться между собой, какие подходы к коммуникации существуют и как выбрать подходящий в зависимости от требований вашей системы. Продолжим наше путешествие в мир DDD, чтобы научиться создавать не только хорошо структурированные, но и слаженно работающие системы.
Основные понятия
Коммуникация между ограниченными контекстами подразумевает обмен данными и вызов функциональности одного контекста из другого. Такой обмен может быть как синхронным (прямой запрос-ответ), так и асинхронным (через события или сообщения), в зависимости от требований системы:
-
Производительность: насколько быстро должны обрабатываться запросы
-
Отказоустойчивость: как система должна реагировать на сбои
-
Согласованность данных: насколько критична мгновенная согласованность
Важно: Неправильно организованная коммуникация может привести к сильной связанности контекстов, что противоречит принципам DDD и усложняет поддержку и развитие системы.
В DDD существует несколько ключевых стратегий коммуникации между bounded контекстами, каждая из которых имеет свои преимущества, недостатки и сценарии применения. Эти стратегии помогают минимизировать связанность контекстов, сохраняя при этом гибкость и масштабируемость системы. В этой статье мы детально рассмотрим каждую из этих стратегий, их особенности, а также примеры использования в реальных проектах.
Примеры контекстов
Для наглядности все примеры в статье будут использовать следующие контексты:
-
UserContext: Управляет информацией о пользователях (профили, аутентификация, разрешения)
-
OrderContext: Управляет заказами (создание, обработка, доставка)
Стратегии интеграции контекстов
Чтобы избежать сильной связанности, в DDD используются следующие подходы:
Anti-Corruption Layer (ACL): Слой, который изолирует один контекст от другого, преобразуя данные и запросы между ними. Защищает целостность модели каждого контекста.
Messages/Events: Асинхронная коммуникация через события, которые один контекст публикует, а другие подписываются на них. Обеспечивает слабую связанность.
API Gateway: Использование промежуточного слоя для организации взаимодействия между контекстами. Централизует и упрощает коммуникацию.
Shared Kernel: В редких случаях, когда контексты должны тесно взаимодействовать, можно выделить общую часть с общими моделями и функциональностью. Должно применяться как исключение, а не правило, поскольку создает тесную связь между контекстами.
Когда применима синхронная коммуникация
Синхронное взаимодействие (например, через REST API или gRPC) стоит выбирать в следующих случаях:
Требуется мгновенная согласованность данных
В некоторых сценариях бизнес-логика требует, чтобы данные были согласованы мгновенно. Например, при создании заказа в интернет-магазине необходимо сразу проверить доступность товара на складе. Если товара нет в наличии, заказ не может быть оформлен, и пользователь должен быть уведомлен об этом сразу же.
В таких случаях синхронная коммуникация между контекстами становится необходимым решением. Синхронный вызов позволяет гарантировать, что данные, используемые для принятия решения, актуальны на момент выполнения операции. Это особенно важно в системах, где задержки или расхождения в данных могут привести к негативному пользовательскому опыту или финансовым потерям.
Простота реализации важнее масштабируемости
Синхронная коммуникация часто выбирается в случаях, когда простота реализации и поддержки системы имеет приоритет над масштабируемостью. Синхронные вызовы проще проектировать, тестировать и отлаживать, так как они следуют линейной логике выполнения: запрос → обработка → ответ.
Это делает их идеальными для:
-
Небольших систем или команд с ограниченными ресурсами
-
Проектов, где настройка сложных асинхронных механизмов может быть избыточной
-
Ситуаций, когда нужно избежать дополнительных сложностей (управление очередями, обработка повторяющихся сообщений, обеспечение идемпотентности)
В небольших проектах или на ранних этапах разработки это может значительно ускорить процесс внедрения и снизить затраты на разработку.
Низкая нагрузка и минимальная задержка
Синхронная коммуникация может быть оптимальным решением в системах с низкой нагрузкой, где количество запросов в секунду невелико, а задержки не критичны. Например:
-
В системах управления внутренними процессами компании, где количество пользователей ограничено
-
В приложениях, где операции выполняются нечасто
-
В системах реального времени, где задержка между запросом и ответом должна быть минимальной
В таких случаях синхронные вызовы могут обеспечить достаточную производительность без необходимости внедрения сложных асинхронных механизмов и избежать дополнительных накладных расходов.
Важно: При увеличении нагрузки синхронные вызовы могут стать узким местом, поэтому такой подход требует тщательного анализа текущих и будущих требований к системе.
Критически важные операции
В некоторых доменах операции являются критически важными и требуют мгновенного подтверждения. Например, в банковских системах каждая транзакция должна быть подтверждена мгновенно, чтобы избежать двойного списания средств или других финансовых ошибок.
В таких случаях синхронная коммуникация между контекстами (например, между контекстом управления счетами и контекстом обработки транзакций) становится необходимым условием для обеспечения целостности данных. Синхронные вызовы позволяют гарантировать, что:
-
Каждая операция будет выполнена только при условии, что все проверки успешно завершены
-
Подтверждение операции происходит мгновенно
-
Данные остаются согласованными во всех контекстах
Это особенно важно в системах, где ошибки или задержки могут привести к серьезным последствиям, таким как финансовые потери или нарушение регуляторных требований.
Когда применима асинхронная коммуникация
Асинхронное взаимодействие (через события, очереди или брокеры сообщений) стоит выбирать в следующих случаях:
Система должна быть устойчивой к сбоям
Асинхронная коммуникация является ключевым инструментом для построения отказоустойчивых систем. В отличие от синхронных вызовов, где сбой в одном контексте может привести к остановке всей системы, асинхронная модель позволяет изолировать ошибки.
Например, если один контекст временно недоступен из-за технических проблем, другой контекст может продолжать работу, помещая запросы в очередь для последующей обработки. Это особенно важно в распределённых системах, где отказ одного компонента не должен влиять на доступность других.
Кроме того, асинхронная коммуникация позволяет реализовать механизмы повторных попыток (retry) и компенсирующих транзакций (saga pattern), что повышает общую надёжность системы.
Требуется высокая масштабируемость
Асинхронные системы обладают высокой масштабируемостью, что делает их идеальными для highload-приложений. В отличие от синхронной коммуникации, где контексты блокируют друг друга, асинхронная модель позволяет обрабатывать запросы параллельно.
Например, в системах, где тысячи пользователей одновременно оформляют заказы, асинхронная обработка позволяет распределить нагрузку между несколькими экземплярами сервисов. Это достигается за счёт использования брокеров сообщений (например, RabbitMQ, Kafka), которые обеспечивают балансировку нагрузки и горизонтальное масштабирование.
Таким образом, асинхронная коммуникация не только повышает производительность, но и позволяет системе адаптироваться к растущим требованиям.
Бизнес-процессы длительные и распределённые
В сложных бизнес-процессах, которые включают множество этапов, асинхронная коммуникация становится незаменимой. Например, оформление заказа в интернет-магазине может включать проверку клиента, резервирование товара, обработку оплаты и отправку уведомлений.
Каждый из этих этапов может выполняться независимо, а асинхронная модель позволяет разбить процесс на отдельные шаги, которые могут обрабатываться разными контекстами. Это не только повышает гибкость системы, но и упрощает её поддержку.
Например, если в процессе оплаты произошла ошибка, система может продолжить обработку других заказов, а проблемный этап будет повторно обработан позже. Такая модель также позволяет легко добавлять новые этапы в процесс без необходимости переписывать существующую логику.
Контексты должны эволюционировать независимо
Одним из ключевых принципов DDD является минимизация связности между контекстами. Асинхронная коммуникация, основанная на событиях, позволяет достичь этой цели.
Контексты взаимодействуют через события, которые представляют собой факты, произошедшие в системе (например, «Заказ создан» или «Товар зарезервирован»). Это позволяет каждому контексту эволюционировать независимо, не затрагивая другие части системы.
Например, если контекст управления складом изменит свою внутреннюю логику, это не потребует изменений в контексте заказов, пока формат событий остаётся прежним. Такая гибкость особенно важна в крупных системах, где разные команды разрабатывают и поддерживают отдельные контексты.
Итоговая согласованность допустима
Асинхронная коммуникация идеально подходит для сценариев, где допустима итоговая согласованность данных. Например, уведомление о статусе заказа может быть отправлено с задержкой, или обновление данных в одном контексте может быть отложено.
Это особенно полезно в системах, где мгновенная согласованность не является критичной, но важна общая производительность и масштабируемость. Например, в системах аналитики или отчетности данные могут обновляться с некоторой задержкой, что не влияет на основные бизнес-процессы.
Асинхронная модель позволяет системе обрабатывать большие объемы данных без необходимости блокировки ресурсов, что делает её более эффективной.
Гибридный подход: Лучшее из двух миров
В реальных системах часто используется комбинация синхронных и асинхронных взаимодействий.
Например:
-
Синхронно: Проверка доступности товара при создании заказа.
-
Асинхронно: Уведомление клиента о статусе заказа или обновление аналитики.
Гибридный подход позволяет сочетать преимущества обоих типов коммуникации, минимизируя их недостатки.
Связанности кода
Прежде чем мы рассмотрим все эти подходы, давайте обратимся к принципам GRASP (General Responsibility Assignment Software Patterns), чтобы понять, какие проблемы они решают. Они помогают проектировать системы с низкой связанностью и высокой связностью, что является ключевым для гибкости и устойчивости. В контексте взаимодействия между bounded contexts, прямое обращение одного контекста к сервисам или репозиториям другого контекста нарушает эти принципы. Давайте разберемся, почему это считается плохой практикой и почему нам не стоит так делать.
Какие проблемы возникают при сильной связанности?
Сложность внесения изменений: Любое изменение в одном контексте может потребовать изменений в другом, что увеличивает стоимость разработки и тестирования.
Снижение гибкости: Система становится менее гибкой, так как изменения в одном месте могут иметь непредсказуемые последствия в других местах.
Потеря ясности модели: Смешение ответственности между контекстами приводит к тому, что модель предметной области становится менее понятной и более запутанной.
Усложнение тестирования: Тестирование становится сложнее, так как контексты зависят друг от друга, и для тестирования одного контекста может потребоваться поднимать весь связанный контекст.
Почему нельзя просто вызвать сервис или репозитория другого контекста?
Прямое взаимодействие между bounded contexts через вызов сервисов или репозиториев нарушает несколько ключевых принципов проектирования, включая принципы GRASP:
1. Нарушение принципа Low Coupling (Низкая связанность)
Low Coupling — это один из ключевых принципов GRASP, который гласит, что компоненты системы должны быть минимально связаны друг с другом. Это позволяет изменять один компонент, не затрагивая другие, что упрощает поддержку и развитие системы.
Если один bounded context напрямую вызывает сервис или репозиторий другого контекста, это создает сильную связанность между ними. Изменения в одном контексте (например, изменение API сервиса или структуры данных) могут потребовать изменений в другом контексте. Это приводит к хрупкости системы и увеличению стоимости внесения изменений.
2. Нарушение принципа High Cohesion (Высокая зацепление)
High Cohesion — другой важный принцип GRASP, который предполагает, что компоненты системы должны быть сфокусированы на выполнении одной четко определенной задачи. Это делает систему более понятной и поддерживаемой.
Когда bounded context начинает напрямую взаимодействовать с внутренними компонентами другого контекста (например, с репозиторием), он берет на себя ответственность, которая должна принадлежать другому контексту. Это нарушает границы ответственности и снижает связность каждого контекста.
Позволяя одному контексту вторгаться во внутреннюю работу другого, мы размываем границы ответственности и усложняем понимание и поддержку системы. Каждый ограниченный контекст должен инкапсулировать собственную логику и данные, обеспечивая высокую сплоченность внутри себя.
Для поддержания высокой связности ограниченные контексты должны взаимодействовать через четко определенные интерфейсы, а не напрямую получать доступ к внутренним компонентам друг друга. Это гарантирует, что каждый контекст остается сосредоточенным на своих собственных обязанностях, придерживаясь принципа высокой связности.
3. Нарушение принципа Information Expert (Информационный эксперт)
Information Expert — принцип, который гласит, что ответственность за выполнение задачи должна быть назначена тому компоненту, который обладает всей необходимой информацией для ее выполнения.
Если один контекст напрямую обращается к репозиторию другого контекста, он берет на себя ответственность за работу с данными, которые должны управляться другим контекстом. Это нарушает принцип Information Expert, так как ответственность за данные распределяется между несколькими контекстами, что усложняет систему.
Придерживаясь принципа информационного эксперта, каждый ограниченный контекст должен управлять своими собственными данными и логикой, гарантируя, что обязанности четко определены и инкапсулированы в соответствующем контексте.
4. Нарушение принципа Protected Variations (Защита от изменений)
Protected Variations — принцип, который предполагает, что система должна быть спроектирована так, чтобы изменения в одном компоненте минимально влияли на другие компоненты.
Прямое взаимодействие между контекстами делает их уязвимыми к изменениям. Например, если один контекст изменит структуру своих данных или API, это может сломать другой контекст, который зависит от этих данных или API. Это нарушает принцип Protected Variations.
Чтобы защититься от таких вариаций, ограниченные контексты должны взаимодействовать через стабильные интерфейсы, а не напрямую получать доступ к внутренним компонентам друг друга. Это гарантирует, что изменения в одном контексте не будут распространяться по всей системе, поддерживая стабильность и снижая риск непреднамеренных побочных эффектов.
Нарушение границ bounded contexts
Ограниченные контексты в DDD создаются именно для того, чтобы изолировать различные модели предметной области и минимизировать их взаимное влияние. Прямое взаимодействие через сервисы или репозитории стирает эти границы, что приводит к смешению моделей и потере четкости в системе.
Когда один ограниченный контекст напрямую обращается к внутренним компонентам другого (например, вызывает его сервисы или репозитории), это нарушает инкапсуляцию, которую DDD стремится достичь. Это приводит к:
-
Смешению ответственности: Ответственность каждого контекста становится неясной, так как один контекст начинает управлять данными или логикой, которые принадлежат другому.
-
Потере ясности модели предметной области: Различные модели предметной области внутри каждого контекста переплетаются, что затрудняет понимание и поддержку системы.
-
Увеличению сложности: Система становится более сложной, так как контексты становятся тесно связанными, что снижает преимущества модульного проектирования.
Ограниченные контексты предназначены для представления автономных единиц с собственными моделями, правилами и логикой. Нарушая их границы, мы подрываем саму цель DDD, которая заключается в управлении сложностью за счет четкого разделения ответственности.
Как этого избежать?
Чтобы сохранить целостность ограниченных контекстов, взаимодействие между ними должно быть:
-
Явным: Используйте четко определенные интерфейсы (например, API, события) для взаимодействия между контекстами.
-
Разделенным: Избегайте прямых зависимостей, используя такие шаблоны, как антикоррупционный слой (Anti-Corruption Layer, ACL), событийно-ориентированная коммуникация или API Gateway.
-
Согласованным с предметной областью: Убедитесь, что взаимодействие соответствует модели предметной области и уважает границы каждого контекста.
Сохраняя четкие границы между ограниченными контекстами, вы обеспечиваете, что каждый контекст остается сосредоточенным на своей собственной модели предметной области, соблюдая принципы DDD и создавая более поддерживаемую и масштабируемую систему.
Синхронная коммуникация
Синхронное взаимодействие — это важный инструмент для сценариев, где требуется мгновенная согласованность данных. Однако такой подход создает сильную зависимость между контекстами, что может снизить гибкость системы и увеличить сложность ее поддержки. Чтобы минимизировать эту зависимость, необходимо тщательно проектировать механизмы взаимодействия между контекстами, даже если они находятся в одном репозитории (монолите).
«Синхронная коммуникация часто приводит к распределенному монолиту, если контексты не изолированы должным образом.» — Martin Fowler, Patterns of Enterprise Application Architecture.
«Синхронное взаимодействие подходит для сценариев, требующих мгновенной согласованности данных, но создает жесткую зависимость между контекстами.» — Vaughn Vernon, Implementing Domain-Driven Design.
Синхронная коммуникация через API
Даже если оба контекста находятся в рамках одного монолита, использование API остается критически важным для соблюдения принципов модульности, слабой связанности и разделения ответственности. API позволяет четко определить границы взаимодействия и обеспечивает прозрачность в обмене данными. Основные подходы к реализации синхронной коммуникации:
-
REST API: Прост в реализации и интеграции, а также хорошо поддерживается в современных экосистемах разработки. Этот подход идеален для случаев, когда требуется стандартизированное и легко поддерживаемое взаимодействие. REST API использует HTTP-протокол, что делает его универсальным и понятным для большинства разработчиков.
-
gRPC: Для высокопроизводительных сценариев, где важна скорость и эффективность обмена данными, gRPC является отличным выбором. Он использует бинарный протокол и поддерживает потоковую передачу данных, что делает его предпочтительным для систем с высокими требованиями к производительности. gRPC особенно полезен в микросервисных архитектурах, где задержки должны быть минимальными.
-
GraphQL: Если требуется гибкость в запросах и возможность получения только необходимых данных, GraphQL становится мощным инструментом. Он позволяет клиентам формировать запросы с точным указанием требуемых полей, что уменьшает объем передаваемых данных и упрощает взаимодействие между контекстами. GraphQL особенно полезен в системах, где клиенты имеют разнообразные требования к данным.
Пример реализации:
Каждый из контекстов реализует внутрений API
Контексты между собой взаимодействуют через этот API
Синхронная коммуникация через Anti‑Corruption Layer (ACL)
Чтобы уменьшить зависимость между контекстами, можно также использовать ACL. Этот слой служит буфером между контекстами, преобразуя данные и запросы из одного контекста в формат, понятный другому. ACL помогает изолировать изменения в одном контексте от другого, что повышает устойчивость системы к изменениям.

Структура ACL обычно включает:
-
AntiCorruption/: Слой антикоррупции (адаптеры, мапперы).
-
Adapters/: Адаптеры для взаимодействия с внешними системами.
-
Mappers/: Мапперы для преобразования данных.
-
Если OrderContext и UserContext должны взаимодействовать, но при этом не могут использовать модели друг друга напрямую, потому что у каждого свои Aggregate Root и бизнес-логика, то ACL становится необходимым. Так как UserContext управляет пользователями, он не предоставляет UserEntity другим контекстам. Вместо этого OrderContext делает запрос и получает DTO, а затем преобразует данные внутри своего контекста.
Пример реализации:
Определить контракт ACL
-
Контекст, который хочет получить данные, создаёт интерфейс ExternalServiceACL, описывающий методы взаимодействия с контекстом, который передаёт данные.
Создать адаптер ACL
-
Контекст, который хочет получить данные, реализует ExternalServiceAdapter, который выполняет вызовы в контекст, который передаёт данные (например, через API, репозиторий или события).
-
Адаптер преобразует данные в формат, удобный для своего контекста.
Определить источник данных
-
Контекст, который передаёт данные, предоставляет API, репозиторий или событие для передачи информации.
Настроить использование ACL
-
Контекст, который хочет получить данные, использует ExternalServiceACL вместо прямых вызовов.
-
Вся логика преобразования данных скрыта внутри адаптера.
Обеспечить изоляцию изменений
-
Если контракт взаимодействия изменится, адаптер ACL обновляется без необходимости изменения основной логики в контексте, который хочет получить данные.
Синхронная коммуникация через имитацию
На ранних этапах разработки, когда реальное API ещё не готово, можно использовать имитацию взаимодействия. Этот подход позволяет разрабатывать и тестировать контексты независимо, используя контракт API на уровне инфраструктуры, но с локальной реализацией.
Контракт API (интерфейс или спецификация) определяет методы и данные, доступные для взаимодействия между контекстами. Он остаётся неизменным, обеспечивая согласованность между реальным и имитированным взаимодействием. Вместо реального HTTP-запроса или gRPC-вызова, на уровне инфраструктуры вызывающего контекста используется заглушка (stub) или фейковый сервис, который возвращает предопределённые данные. Это позволяет тестировать интеграцию без готового удалённого сервиса.
Преимущества имитации:
-
Независимая разработка – контексты можно разрабатывать и тестировать отдельно.
-
Упрощённое тестирование – сценарии проверяются без зависимости от внешних систем.
-
Гибкость – легко переключаться между имитацией и реальным взаимодействием, изменяя лишь реализацию в инфраструктурном слое.
Пример реализации
Определить контракт API
-
Контекст, который хочет получить данные, создаёт интерфейс ExternalServiceClient, описывающий методы взаимодействия с контекстом, который передаёт данные.
-
Этот контракт определяет сигнатуры методов, как если бы они вызывали реальный API.
Создать клиент API
-
Контекст, который хочет получить данные, реализует ExternalServiceHttpClient, который обычно отправлял бы HTTP-запросы или использовал gRPC.
-
На этом этапе реальная логика взаимодействия с внешним API ещё не реализована.
Создать заглушку (имитацию)
-
В инфраструктурном слое ExternalServiceHttpClient вместо реального HTTP-запроса вызывает ApplicationService контекста, который передаёт данные, напрямую.
-
Например, если контекст, который передаёт данные, имеет сервис UserApplicationService, API-клиент вместо реального запроса вызывает этот сервис внутри текущего процесса.
Использовать API-клиент в коде
-
Контекст, который хочет получить данные, работает с ExternalServiceClient, не зная, что вместо реального API используется внутренняя реализация.
Легко заменить имитацию на реальное API
Когда реальный API будет готов, достаточно заменить ExternalServiceHttpClient на его реальную версию, которая выполняет сетевые вызовы. В коде бизнес-логики изменений не потребуется, так как взаимодействие идёт через интерфейс ExternalServiceClient. Это обеспечивает плавный переход от имитации к реальному взаимодействию.
Синхронная коммуникация через Shared Kernel
Shared Kernel — это общая часть модели или кода, которая используется двумя или более bounded contexts. Это может быть:
-
Общие классы доменной модели (например, сущности, value objects).
-
Общие сервисы или утилиты.
-
Общие библиотеки или модули.
Shared Kernel используется, когда несколько контекстов должны работать с одними и теми же данными или логикой, но при этом важно минимизировать дублирование кода и обеспечить согласованность.

Подходит такой подход в следующих случаях:
-
Высокая степень взаимодействия: Когда два контекста тесно связаны и должны часто обмениваться данными или логикой.
-
Общие бизнес-правила: Когда несколько контекстов используют одни и те же бизнес-правила или данные, и важно избежать их дублирования.
-
Ограниченная команда: Когда оба контекста разрабатываются одной командой или командами, которые тесно сотрудничают.
Однако Shared Kernel следует использовать с осторожностью, так как он увеличивает связанность между контекстами. Если контексты разрабатываются разными командами или их модели могут эволюционировать независимо, лучше использовать другие стратегии.
Пример реализации:
Определите, какие части модели или кода являются общими для обоих контекстов
Вынесите эти общие части в отдельный модуль в SharedKernal, которая будет использоваться обоими контекстами.
Когда не стоит использовать Shared Kernel?
Контексты разрабатываются разными командами: Если команды работают независимо, Shared Kernel может стать источником конфликтов.
Модели контекстов могут эволюционировать независимо: Если модели контекстов могут меняться независимо, Shared Kernel будет ограничивать их развитие.
Низкая степень взаимодействия: Если контексты редко взаимодействуют, Shared Kernel может быть излишним.
Важно! Поскольку Shared Kernel используется несколькими контекстами, любые изменения в нем должны быть согласованы между командами, работающими над этими контекстами.
Альтернативы Shared Kernel
Вынести общей код в пакеты. Это популярная альтернатива Shared Kernel, которая позволяет уменьшить связность между bounded contexts, сохраняя возможность повторного использования кода. Суть подхода заключается в том, что общий код (например, сущности, value objects, сервисы, утилиты) выносится в отдельный пакет, который затем подключается как зависимость в нескольких bounded contexts.Этот подход позволяет сохранить независимость контекстов, так как изменения в общем пакете не влияют на контексты, если они не используют измененный код.
Преимущества:
-
Повторное использование кода без дублирования: Общий код хранится в одном месте, что исключает его дублирование в разных контекстах.
-
Управление версиями общего кода: Общий пакет может иметь собственную систему контроля версий, что упрощает управление изменениями и обновлениями.
-
Минимизация связанности между контекстами: Каждый bounded context зависит только от общего пакета, а не от других контекстов напрямую, что снижает связанность.
Пример реализации:
Определите, какие части модели или кода являются общими для обоих контекстов.
Вынесите эти общие части в отдельный пакет/библиотеку, которая будет использоваться обоими контекстами как зависимость.
В отличии от Shared Kernel пакеты могут иметь версионность, что позволяет использовать разные версии несколькими контекстами.
Асинхронная коммуникация
Асинхронная коммуникация — это не просто технический паттерн, а стратегия декомпозиции бизнес-процессов. Как отмечает Эрик Эванс в «Domain-Driven Design», она позволяет контекстам:
-
Эволюционировать независимо (изменять модель без согласования с потребителями)
-
Минимизировать временную связность (контексты не блокируют друг друга)
-
Отражать реальные бизнес-процессы (где многие операции имеют отложенный характер)
«События — это следы, оставляемые доменом в процессе своей эволюции. Они не связывают контексты, а лишь фиксируют факты».
— Эрик Эванс, Domain-Driven Design: Tackling Complexity in the Heart of Software
Очереди сообщений (Message Queue)
Очереди сообщений (например, RabbitMQ, Apache Kafka, Amazon SQS) используются для передачи сообщений между контекстами в асинхронном режиме. В этом случае UserContext и OrderContext обмениваются сообщениями через брокер, а не напрямую вызывают API друг друга.
Преимущества очередей сообщений:
-
Независимость контекстов – Отправитель и получатель не зависят друг от друга.
-
Надежность – Если контекст временно недоступен, сообщение остается в очереди.
-
Масштабируемость – Можно обрабатывать сообщения параллельно несколькими сервисами.
Очереди сообщений используются для передачи команд и задач, которые должны быть обработаны гарантированно.
Примеры:
-
SendWelcomeEmailQueue – Отправка приветственного письма после регистрации пользователя. UserContext добавляет задачу в очередь. NotificationService обрабатывает и отправляет письмо.
-
ProcessPaymentQueue – Обработка платежа. OrderContext ставит задачу в очередь. PaymentContext выполняет платеж.
-
GenerateInvoiceQueue – Генерация счета-фактуры после покупки. OrderContext создает задачу. BillingService генерирует PDF и отправляет клиенту.
-
SendOrderConfirmationQueue – Отправка подтверждения заказа. OrderContext ставит задачу в очередь. NotificationContext отправляет email и SMS.
-
ReserveStockQueue – Резервирование товара на складе. OrderContext добавляет задачу. InventoryContext резервирует товар.
-
ProcessShippingQueue – Отправка заказа клиенту. OrderContext добавляет задачу. LogisticsContext передает данные в службу доставки.
-
RetryFailedPaymentQueue – Повторная попытка списания средств. PaymentContext добавляет задачу. BillingService пытается списать средства повторно.
Пример реализации:
Постановка сообщения в очередь
Обработка в другом контексте
Событийная модель (Event-Driven Architecture)
В событийной модели контексты обмениваются не просто сообщениями, а доменными событиями (Domin Events). Это означает, что UserContext не просто говорит «UserCreatedOrder», а публикует событие, на которое OrderContext реагирует.
В DDD разница между доменными событиями (Events) и очередями сообщений (Message Queues) заключается в их предназначении:
-
Доменные события отражают факты, произошедшие в бизнесе (например, «Заказ был оплачен»). Они не требуют немедленного ответа и публикуются в шину событий.
-
Очереди сообщений используются для выполнения команд и задач, которые должны быть гарантированно обработаны (например, «Отправить email пользователю»).
Доменные события публикуются в системе и могут иметь несколько подписчиков. Они не предполагают немедленного ответа.
Приемры:
-
UserRegistered – Пользователь зарегистрировался в системе.
-
UserUpdatedProfile – Пользователь изменил данные профиля. UserContext публикует это событие. NotificationContext отправляет email с подтверждением. LoyaltyContext начисляет приветственные бонусы.
-
OrderPlaced – Пользователь оформил заказ. OrderContext публикует событие. PaymentContext обрабатывает оплату. WarehouseContext резервирует товары.
-
OrderPaid – Заказ успешно оплачен. PaymentContext публикует событие. OrderContext переводит заказ в статус «Готов к отправке». LoyaltyContext начисляет кешбэк.
-
OrderShipped – Заказ отправлен пользователю. LogisticsContext публикует событие. OrderContext обновляет статус заказа. UserContext отправляет уведомление покупателю.
-
ProductAddedToCart – Пользователь добавил товар в корзину. CartContext публикует событие. RecommendationContext анализирует данные для персонализированных предложений.
-
ProductStockLow – Остаток товара ниже порогового значения. InventoryContext публикует событие. SupplierContext отправляет запрос на закупку.
Оба подхода — доменные события и очереди сообщений — играют важную роль в построении масштабируемых, отказоустойчивых и слабо связанных систем:
-
События используются для оповещения других частей системы о произошедших фактах.
-
Очереди сообщений обеспечивают гарантированное выполнение критичных команд.
Пример реализации:
Определение Доменного События
Генерация и публикация события
Подписка на событие
Конфигурация Messenger на уровне инфраструктуры
Проблема Eventual Consistency
Коммуникация между ограниченными контекстами через сообщения позволяет повысить масштабируемость и отказоустойчивость системы, но при этом вносит дополнительные сложности, связанные с обеспечением согласованности данных. Одной из ключевых проблем в этом контексте является eventual consistency (согласованность).
Eventual consistency — это модель согласованности данных, при которой изменения, внесённые в одной части системы, со временем распространяются на все связанные компоненты. В отличие от строгой согласованности (strong consistency), где данные должны быть актуальными сразу после выполнения операции, eventual consistency допускает временную рассогласованность. Это означает, что в какой-то момент времени разные части системы могут «видеть» разные состояния данных. Это неизбежный компромисс в системах с асинхронной коммуникацией. Понимание этой проблемы и её влияния на бизнес-процессы позволяет проектировать более устойчивые и предсказуемые системы. Важно учитывать, что eventual consistency не является недостатком, а скорее особенностью, которую нужно учитывать при проектировании и разработке.
Влияние на бизнес-процессы
Eventual consistency может оказывать значительное влияние на бизнес-процессы, особенно в системах, где требуется мгновенная реакция на изменения. Рассмотрим пример:
Пример: Взаимодействие UserContext и OrderContext
Когда пользователь создаёт заказ, система OrderContext может запросить информацию о пользователе из UserContext, чтобы проверить, активен ли он и может ли совершать заказы. Однако из-за асинхронной природы коммуникации между контекстами может возникнуть задержка между моментом изменения статуса пользователя в UserContext и моментом обновления этой информации в OrderContext.
Проблема: Пользователь может быть заблокирован в UserContext, но OrderContext ещё не получил это обновление и разрешает создание заказа. Это может привести к нарушению бизнес-правил, например, к созданию заказов заблокированными пользователями.
Как справляться с Eventual Consistency?
Компенсационные действия (Compensating Transactions): Если система обнаруживает, что данные не согласованы, она может выполнить компенсационные действия. Например, если товар закончился после создания заказа, система может отменить заказ и уведомить пользователя.
Оповещение пользователя: В интерфейсе можно предусмотреть уведомления о том, что данные обновляются, и попросить пользователя подождать. Например: «Проверяем доступность товара, пожалуйста, подождите…»
Сглаживание задержек: Использование кеширования или предварительных резервирований может помочь уменьшить видимость задержек для пользователя.
Проектирование с учётом eventual consistency: На этапе проектирования системы важно учитывать, что данные могут быть временно несогласованными. Это может повлиять на выбор архитектурных решений, таких как использование Saga-паттерна для управления распределёнными транзакциями.
Визуализация взаимодействий
После того как вы определили, как ваши Bounded Contexts взаимодействуют друг с другом, наступает время зафиксировать эти отношения в виде карты контекстов (Context Map). Этот инструмент, введённый Эриком Эвансом в книге «Domain-Driven Design», помогает визуализировать и документировать связи между контекстами, что особенно важно для больших и сложных систем.
Карта контекстов — это не просто диаграмма, а стратегический артефакт, который помогает командам:
-
Понимать, как контексты связаны между собой.
-
Управлять зависимостями и минимизировать их.
-
Планировать эволюцию системы.
-
Общаться на языке, понятном как разработчикам, так и бизнесу.
Карта контекстов — это графическое представление Bounded Contexts и их взаимодействий. Она показывает:
-
Какие контексты существуют в системе.
-
Как они связаны между собой (типы отношений).
-
Какие данные или команды передаются между ними.
-
Какие стратегии интеграции используются.
Карта контекстов — это живой документ, который должен обновляться по мере изменения системы.
Как построить карту контекстов?
Шаг 1: Определите контексты
Начните с идентификации всех ограниченных контекстов в вашей системе. Ограниченный контекст — это логическая область, которая имеет четкие границы и собственную модель данных. Например, в системе электронной коммерции могут быть следующие контексты:
-
UserContext: Управление пользователями, аутентификация, профили, роли и разрешения.
-
OrderContext: Оформление заказов, управление корзиной, оплата, статусы заказов.
-
ProductContext: Управление каталогом товаров, категориями,库存 и ценами.
-
NotificationContext: Отправка уведомлений (email, SMS, push) пользователям.
-
AnalyticsContext: Сбор и анализ данных о поведении пользователей и продажах.
Каждый контекст должен быть описан с точки зрения его ответственности и ключевых функций.
Шаг 2: Определите отношения между контекстами
После того как вы определили контексты, необходимо выявить, как они взаимодействуют друг с другом.
Пример отношений:
-
UserContext и OrderContext: Клиент-Поставщик. UserContext предоставляет данные о пользователе для оформления заказа.
-
OrderContext и NotificationContext: Клиент-Поставщик. OrderContext отправляет события о статусе заказа в NotificationContext для уведомления пользователя.
Шаг 3: Визуализируйте связи
Используйте диаграммы для отображения отношений между контекстами. Это может быть схема, где каждый контекст представлен в виде блока, а связи между ними — в виде стрелок с указанием типа взаимодействия.
Преимущества карт контекстов
-
Прозрачность: Все участники команды (разработчики, архитекторы, менеджеры) понимают, как устроена система и как взаимодействуют её части.
-
Управление зависимостями: Вы можете выявить слабые места и минимизировать нежелательные связи между контекстами.
-
Планирование изменений: Карта помогает оценить сложность внедрения новых функций и понять, какие контексты будут затронуты.
-
Общение с бизнесом: Визуализация помогает объяснить архитектуру системы нетехническим заинтересованным сторонам.
Практические рекомендации
Используйте инструменты: Для создания диаграмм используйте такие инструменты, как Miro, Lucidchart, Draw.io или даже простые UML-диаграммы.
Документируйте: Добавьте описание каждого контекста, его роли и ответственности. Укажите, какие команды, события или данные передаются между контекстами.
Обновляйте карту: Карта контекстов должна отражать текущее состояние системы. Регулярно пересматривайте её в рамках архитектурных обзоров.
Вовлекайте команду: Карта контекстов — это коллективный артефакт. Убедитесь, что все члены команды понимают её и используют в своей работе.
Архитектурные нюансы
При проектировании и реализации коммуникации между контекстами в распределенной системе можно столкнуться с множеством подводных камней. Эти проблемы могут привести к снижению производительности, увеличению сложности поддержки, нарушению согласованности данных и даже полному отказу системы. Ниже приведены основные подводные камни и способы их устранения или минимизации.
Проблемы интеграции и их решения
Антикоррупционный слой (ACL)
Чтобы избежать проникновения чужой модели в ваш контекст, UserApiClient в инфраструктурном слое OrderContext должен преобразовывать DTO из UserContext в собственную доменную модель.
Согласование контрактов
Контексты должны согласовать форматы запросов/ответов (например, через OpenAPI для REST). Это минимизирует риски несовместимости.
Таймауты и повторы
Синхронные вызовы требуют обработки сетевых ошибок:
-
Используйте механизмы повторных попыток (retry) с экспоненциальной задержкой
-
Внедрите Circuit Breaker (автоматический выключатель), чтобы временно отключать неработающие контексты
-
Переходите на асинхронную коммуникацию, где это возможно
Производительность
Задержки сети и обработки запросов снижают отзывчивость системы:
-
Используйте кэширование для уменьшения количества запросов
-
Переходите на асинхронную коммуникацию, где это возможно
-
Оптимизируйте запросы, передавая только необходимые данные
Масштабируемость
Вертикальное масштабирование становится сложнее из-за синхронных зависимостей.
Распространенные ошибки при проектировании
Неправильное определение границ и смешение терминологии
Если границы контекстов определены нечетко или один и тот же термин используется по-разному, возникает риск «утечки» модели одного контекста в другой. Это может привести к конфликтам в бизнес-логике и усложнить поддержку кода.
Проблемы версионирования и несовместимости контрактов
При изменениях в API или схеме сообщений отсутствие четкого процесса версионирования может привести к поломке взаимодействия между контекстами. Непредвиденные изменения могут привести к тому, что потребители не смогут корректно интерпретировать полученные данные.
Сложности в мониторинге и отладке
Асинхронные системы часто труднее отлаживать:
-
Сложнее проследить полный цикл обработки сообщения
-
Труднее обнаружить, где именно возникла ошибка
-
Сложнее обеспечить трассируемость взаимодействия между контекстами
Проблемы безопасности
Если обмен данными между контекстами не защищен должным образом (например, отсутствует шифрование или механизмы аутентификации), существует риск утечки конфиденциальной информации или несанкционированного доступа.
Выводы
Выбор между синхронной и асинхронной коммуникацией — это всегда компромисс. Синхронные вызовы проще и быстрее внедрить, но они создают жёсткие зависимости и ограничивают масштабируемость. Асинхронная коммуникация требует больше усилий, но обеспечивает гибкость, устойчивость и возможность эволюции системы.
«Архитектура — это не поиск идеального решения, а поиск баланса между противоречивыми требованиями».
— Эрик Эванс
Рекомендации по выбору подхода
При выборе стратегии коммуникации между ограниченными контекстами рекомендуется учитывать следующие факторы:
-
Бизнес-требования: Насколько критична немедленная согласованность данных? Могут ли бизнес-процессы работать в асинхронном режиме?
-
Технические ограничения: Какие технологии уже используются в проекте? Насколько сложно будет внедрить асинхронную коммуникацию?
-
Масштабируемость: Ожидается ли значительный рост нагрузки на систему в будущем?
-
Отказоустойчивость: Насколько критичны простои системы? Могут ли части системы продолжать работу при недоступности других частей?
-
Эволюция системы: Как часто будут меняться контракты между контекстами? Насколько независимо должны развиваться разные части системы?
Выбирайте подход, который лучше всего соответствует вашим бизнес-целям, техническим ограничениям и долгосрочной стратегии развития системы. И помните: даже если сегодня вы выбираете синхронную коммуникацию, проектируйте систему так, чтобы в будущем можно было перейти к асинхронной модели без полного переписывания кода.
ссылка на оригинал статьи https://habr.com/ru/articles/892250/
Добавить комментарий