Зачем нужен event sourcing?

Если идет работа с реальными людьми, то нужно сохранять все действия для последующего “разбора полетов”. Для многих это основная бизнес-цель event sourcing.

Если посмотреть в будущее, то, другими словами, примерно все бизнес-приложения рано или позже полностью или частично перейдут на эту архитектуру. Почему тогда существующие решения массово не используют? Это все-таки требует дополнительного проектирования и кода, но при постоянном увеличении требований к ПО это уже сейчас по идее окупается.

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

Стоит отметить, что это достаточно революционное отклонение от обычной архитектуры. Другими словами, потребуется дополнительное обучение программистов и новые лучшие практики. Для примера негативного опыта можно почитать, например, этот тред: https://news.ycombinator.com/item?id=13339972

По теории можно прочитать предыдущую заметку “Классический event sourcing”.

Основные проблемы

  • Eventual Consistency или старые данные возвращаются при чтении.
  • Как разбираться с ошибками, которые случились где-то по середине обработки?
  • Медленное построение онлайн проекций (когда событий много в агрегате)
  • При хранении событий в обычной БД c автоинкрементом иногда теряются событий при обработке, т.к. БД под нагрузкой выделяет блоки автоинкрементных id, которые параллельно сохраняются, а обработчик событий обычно ищет по последним id.

Предлагаемое решение

Для решения этих проблем предполагается следующие изменение архитектуры относительно классической:

  • Вводится понятие “основной проекции” – это проекция, расположенная в той же БД, что и хранилище событий.
  • Основная проекция должна содержать все данные, необходимые для работы команд, встроенные проекторы основной БД и реакторов. Соответственно, при необходимости получения данных они берутся из основной проекции, а не из хранилища событий.
  • Хранилище событий используется для получения истории и при необходимости перестроения основной проекции (полное или частичное).
  • Команда, сохранение события и встроенные проекторы основной БД работают в одной транзакции.
  • Реакторы работают в отдельной транзакции.
  • Живые проекции не рекомендуются (только для специфичных случаев с учетом потенциального наличия задержки). Фактически нет такого понятия как класс агрегата или агрегат в памяти. Идет работа на уровне команд и событий, а агрегат как id только для транзакций и параллельности.
  • Если требуется ограниченная асинхронность, то можно прикрутить рассылку событий через NATS (или другую очередь).

Как решаются проблемы?

  • Нет eventual consistency, т.к. идет классическая работа с данными.
  • Обработка ошибок опять же классическая – если все пошло не так, то транзакция откатывается, а клиент получает сообщение об ошибке.
  • Нет медленной обработки событий при получении данных, т.к. для этого, в основном, используется основная проекция.
  • Нет проблемы с диапазонами автоинкрементов, т.к. при обработке событий мы не полагаемся на эти id.

Что теряется?

  • Полностью асинхронный подход (который потенциально облегчает масштабирование) – его можно применить, но уже частично.
  • Распределенность – ее нет из коробки, нужно думать отдельно
  • Масштабирование – ее нет из коробки, нужно думать отдельно

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

Кто-то даже может сказать, что это совсем не Event sourcing, если у нас нет агрегата в памяти, который вычисляет свое состояние по событиям. Но у нас есть агрегат в базе данных, который создан по событиям (чем не память, только не оперативная). Так что это все-таки Event sourcing, но я довольно сильно рад, что не классический, т.к. классический имеет слишком много ограничений для обычных приложений. Я бы сказал, что предлагаемый подход не event driven (EVA), но event sourcing.

Как итог, идет отказ от разделения базы событий и основной проекции. За счет этого решаются проблемы распределенности и асинхронности (их нет). При масштабировании решения я бы скорее использовал партиционирование решения, чем асинхронность и отдельные сервисы. Но, это, понятно, зависит от специфики решения: в разных случаях могут быть разные ответы.

С одной стороны, ничего сверхестественного в подходе нет. С другой, какого-то описания и термина для такого подхода не встречал. Предлагаю использовать Inline event sourcing (встроенный event sourcing), т.к. он основывается на встроенной проекции.