Последнее время получают распространение событийно-ориентированные архитектуры (event-driven architectures) и, в частности, Event Sourcing (порождение событий). Эта вызвано стремлением к созданию устойчивых и масштабируемых модульных систем. В этом контексте довольно часто используется термин “микросервисы”. На мой взгляд, микросервисы — это всего лишь один из способов реализации “ограниченного контекста” (Bounded Context). Очень важно правильно определить границы модулей и в этом помогает стратегическое проектирование (Strategic Design), описанное Эриком Эвансом в Domain Driven Design. Оно помогает вам идентифицировать / обнаружить модули, границы (“ограниченный контекст”) и описать, как эти контексты связаны друг с другом (карта контекстов, ContextMap).
События предметной области как основа единого языка
Хотя в книге Эрика Эванса это явно не обозначено, но события предметной области очень хорошо содействуют концепциям DDD. Такие практики как Event Storming Альберто Брандолини смещают акцент у событий с технического на организационный и бизнес-уровень. Здесь мы говорим не о событиях пользовательского интерфейса, таких как щелчок на кнопке (ButtonClickedEvent), а о доменных событиях, которые являются частью предметной области. О них говорят и их понимают эксперты в предметной области. Эти события представляют собой первоочередные концепции и помогают сформировать единый язык (ubiquitous language), с которым будут согласны все участники (эксперты в предметной области, разработчики и т.д.).
События домена, используемые для связи между контекстами
Доменные события могут использоваться для взаимодействия между ограниченными контекстами. Предположим, у нас есть интернет-магазин с тремя контекстами: Order (заказ), Delivery (доставка), Invoice (счет).
Рассмотрим событие “Заказ принят” в контексте Order. Контекст Invoice, а также контекст Delivery заинтересованы в отслеживании этого события, так как это событие инициирует некоторые внутренние процессы в этих контекстах.
Миф о слабой связанности
Использование доменных событий помогает разрабатывать слабосвязанные модули. Отдельные модули могут быть временно не доступны. Но для доменного события совершенно не важно доступны они или нет, так как событие только описывает то, что произошло в прошлом. Другие модули сами решают, когда обработать событие. У вас по умолчанию получается гибкая система.
Помимо развязки по времени, доменные события дают вам еще одно преимущество: контекст заказа не должен знать, что контекст счетов и доставки слушают его события. На самом деле ему даже не нужно знать, что эти контексты существуют.
Это здорово, но сложность заключается в решении того, какие данные хранить в событии?
Простой ответ: Event Sourcing!
События полезны, так почему бы не использовать их по максимуму. Это основная идея Event Sourcing. Вы храните состояние агрегата не через обновление его данных (CRUD), а через применение потока событий.
Помимо того, что вы можете воспроизвести события и получить состояние, есть еще одна особенность Event Sourcing: вы бесплатно получаете полный журнал аудита. Поэтому, когда требуется такой журнал, при выборе стратегии хранения обязательно обратите внимание на Event Sourcing.
Event Sourcing — это только уровень хранения
Вам может показаться странным, что я сразу перешел от доменных событий к хранению, так как, очевидно, что это концепции разных уровней.
… и вот моя точка зрения: Event Sourcing — это локальное решение, используемое только в одном ограниченном контексте! События Event Sourcing не должны выдаваться наружу во внешний мир! Другие ограниченные контексты не должны знать о способах хранения данных друг друга, и поэтому им не важно, использует ли какой-то контекст Event Sourcing.
Если вы используете Event Sourcing глобально, то вы раскрываете свой уровень хранения.
Способ хранения данных становится вашим публичным API. Каждый раз при внесении изменений в уровень хранения вам придется иметь дело с изменением публичного API.
Я уверен, все согласятся с тем, что плохо, когда разные ограниченные контексты совместно используют данные в (реляционной) базе данных из-за возникающей связанности. Но чем это отличается от Event Sourcing? Ничем. Не имеет значения, используете вы общие события или общие таблицы в базе данных. В обоих случаях вы разделяете детали хранения.
Выход есть
Я все еще утверждаю, что доменные события идеально подходят для взаимодействия между ограниченными контекстами, но эти события не должны быть связаны с событиями, которые используются для Event Sourcing.
Предлагаемое решение очень простое: независимо от того, какой подход вы используете для хранения данных (CRUD или Event Sourcing), вы публикуете доменные события в глобальном хранилище событий. Эти события представляют собой публичное API вашего контекста. При использовании Event Sourcing вы храните события Event Sourcing в своем локальном хранилище, доступном только для этого ограниченного контекста.
Свобода выбора
Наличие отдельных доменных событий в публичном API позволяет вам гибко их моделировать. Вы не ограничены моделью, которая предопределена событиями Event Sourcing.
Есть два варианта для работы с ”событиями реального мира”: служба с открытым протоколом и общедоступным языком (Open Host Service, Published Language) или Заказчик / Поставщик (Customer/Supplier).
Служба с открытым протоколом и общедоступным языком (Open Host Service, Published Language)
Публикуется только одно доменное событие, содержащее все данные, которые могут понадобиться другим ограниченным контекстам. В терминологии DDD это можно назвать службой с открытым протоколом (Open Host Service) и общедоступным языком (Published Language).
Наступление события реального мира “Заказ принят” приводит к тому, что публикуется одно доменное событие OrderAccepted. Полезная нагрузка этого события содержит все данные о заказе, которые, могут понадобиться другим ограниченным контекстам… так что, надеюсь, контексты Invoice и Delivery найдут всю необходимую им информацию.
Заказчик / Поставщик (Customer/Supplier)
Для каждого потребителя публикуются отдельные события. Необходимо согласовать модели каждого события только с одним потребителем, не требуется определять общую разделяемую модель. DDD называет эти отношения Заказчик / Поставщик (Customer/Supplier).
Возникновение события реального мира “Заказ принят” приводит к публикации отдельных событий для каждого из потребителей:
InvoiceOrderAccepted
и DeliveryOrderAccepted
. Каждое доменное событие содержит только те данные, которые необходимы контексту-получателю.Я не хочу сейчас обсуждать плюсы и минусы этих подходов. Я хочу просто обратить внимание на то, что можно выбирать количество доменных событий и данные, которые в них хранить.
Это преимущество, которое вы не должны недооценивать, потому что можно решить, как развивать API вашего ограниченного контекста без привязки к событиям Event Sourcing.
Заключение
Выставление наружу деталей хранения — хорошо известный анти-паттерн. Говоря о хранении, мы в первую очередь думаем о таблицах базы данных, но мы увидели, что события, используемые для Event Sourcing, являются лишь еще одним способом хранения данных. Поэтому выдавать их наружу тоже является анти-паттерном.
Перевод: “хороший разработчик как оборотень — боится серебряных пуль”.
Event Sourcing — это мощный подход, если используется правильно (локально). На первый взгляд кажется, что для событийно-ориентированных архитектур это серебряная пуля, но если присмотреться внимательнее, то видно, что этот подход может привести к сильной связанности … чего вы, конечно, не хотите.
Ссылки
Помимо моего личного опыта, я получил много вдохновения от различных статей и конференций. Я хотел бы отметить выступление Эберхарда Вольфа (Eberhard Wolff) “Event-based Architecture and Implementations with Kafka and Atom” (событийная архитектура и реализация с использованием Kafka и Atom). Особенно про Event Sourcing и про то, что такое события, что очень актуально в контексте этого поста. Пример с интернет-магазином также был вдохновлен этим докладом.
Если вы хотите получить больше информации, вы можете обратиться к этим ресурсам:
- Статья Christian Stettler Domain Events vs. Event Sourcing
- Статья Hugo Rocha What they don’t tell you about event sourcing
- Выступление на конференции Greg Young A Decade of DDD, CQRS, Event Sourcing (CQRS/ES is NOT a top level architecture)
Бесплатный вебинар: «Микросервисная архитектура: достоинства и недостатки».
VolCh
Игрался с Кафкой и пришёл к выводу, что ES имеет смысл разбить на глобальный и локальный. Глобальный — единый аудит лог, из которого можно "накармливать" новые сервисы или сервисы, в которых обнаружен баг, приведший к порче данных. А вот для локальных задач типа получить стейт сущности с таким-то ид, кафка как-то не очень