Сервис хранит состояние сущности только как последовательность неизменяемых событий. Как получить её текущее состояние при чтении?
Текущее состояние получают воспроизведением событий в правильном порядке и последовательным применением их к начальному состоянию. Чтобы не обрабатывать всю историю при каждом чтении, обычно периодически сохраняют снимки состояния, после чего проигрывают только события, появившиеся после снимка.
Такой подход называется хранением событий. Он сохраняет историю изменений, но переносит сложность с обновления записей на проектирование событий, их порядка, версий и процедуры восстановления состояния.
Обычная CRUD-модель хранит преимущественно последнее состояние строки: например, текущий статус заказа или текущий баланс. Она удобна для оперативных запросов, но сама по себе не отвечает, какие изменения произошли, в каком порядке и какое состояние было на конкретный момент времени.
Хранение событий возникло как способ сделать историю изменений первичной моделью данных. Вместо перезаписи состояния система добавляет факт изменения, а состояние рассматривается как результат применения всей последовательности таких фактов.
Пусть для заказа записаны события создания, добавления позиции, оплаты и отмены. В хранилище нет отдельной строки с текущим статусом: его нужно вычислить из истории.
Неверная реализация может применить события в неправильном порядке, пропустить событие или дважды обработать его. Тогда восстановленное состояние будет отличаться от ожидаемого, а повторное чтение может давать другой результат.
Есть и эксплуатационная цена: при длинной истории полное воспроизведение становится дорогим, а изменение правил расчёта может потребовать аккуратной работы с уже сохранёнными событиями.
Каждое событие должно содержать тип изменения, идентификатор сущности, данные события и сведения, позволяющие определить его положение в истории. В пределах одной сущности обработчик применяет события последовательно к начальному состоянию, получая новое состояние после каждого шага.
Ключевое требование — детерминированность: одинаковая последовательность событий должна приводить к одинаковому состоянию. Поэтому бизнес-правила восстановления не должны зависеть от случайного порядка чтения, текущего времени или внешнего изменяемого источника без явно зафиксированной версии.
Для ускорения чтения создают снимок — состояние сущности после некоторого события. При восстановлении сначала загружают ближайший подходящий снимок, затем применяют события после него. Снимок является производным ускоряющим представлением, а не заменой журналу событий: удаление или повреждение снимка не должно уничтожать историю.
Порядок событий нужно определять явно. Временная метка сама по себе может быть недостаточной: два события способны иметь одинаковое время, а часы разных узлов могут расходиться. Обычно используют монотонную позицию в журнале или другой однозначный порядок в пределах агрегата.
Изменение формата события требует версионирования и совместимого преобразования старых вариантов. Нельзя бездумно трактовать старое событие по новым правилам, если это меняет исторический смысл уже зафиксированного факта.
Для запросов списка заказов, поиска и отчётов полное воспроизведение каждой сущности часто не подходит. Поэтому поверх журнала строят проекции или специализированные представления, но нужно учитывать задержку их обновления и возможность повторного построения из исходных событий.
Преимущества подхода — полная история, возможность аудита, повторного построения проекций и анализа состояния на прошлый момент. Недостатки — более сложная модель чтения, необходимость управлять версиями событий, стоимость хранения и требования к согласованности порядка обработки.
Платёжная система должна показывать текущий статус платежа и объяснять, почему он оказался отклонён. Рассматривались три варианта.
Классическая CRUD-модель проста для чтения и обновления, но для аудита пришлось бы отдельно проектировать журнал изменений; при ошибке в журналировании история могла бы неполно отражать фактические переходы. Полное хранение событий даёт естественный аудит и позволяет восстановить статус, но усложняет чтение и требует строгой дисциплины версионирования.
Гибридный вариант хранит неизменяемые события как источник истины и поддерживает текущую проекцию платежа для быстрых запросов. При чтении интерфейс использует проекцию, а при споре или восстановлении она пересоздаётся из событий; для длинных историй применяются снимки.
Выбранный вариант оправдан тем, что аудит является обязательным, а пользовательские запросы требуют низкой задержки. Результат — быстрые обычные чтения, воспроизводимая история переходов и возможность восстановить проекцию после её повреждения.
Нужно хранить и проверять явную последовательность событий в пределах сущности либо использовать упорядоченный журнал с позицией, которую обработчик контролирует. Одной временной метки недостаточно из-за одинаковых времён и рассинхронизации часов.
Если порядок нарушен, результат зависит от случайного порядка доставки или чтения. Это особенно опасно для переходов состояний, операций над балансом и других необратимых действий.
Снимок уменьшает объём работы при чтении: вместо полного журнала обрабатывается снимок и хвост событий после него. Это снижает задержку и нагрузку, но добавляет процесс создания, хранения и проверки снимков.
Снимок нельзя считать независимым источником истины. При ошибке в его построении должна существовать возможность сравнить его с журналом и пересоздать его заново.
Простое применение нового правила ко всей старой истории может изменить ранее получавшееся состояние, хотя события не менялись. Поэтому фиксируют версии событий или правила их интерпретации, а миграцию выполняют явно и проверяемо.
В некоторых системах старые события преобразуют в новый формат, в других сохраняют несколько обработчиков версий. Выбор зависит от требований к воспроизводимости и стоимости поддержки, но неявное изменение смысла истории обычно недопустимо.