АрхитектураПроектирование системАрхитектор распределённых систем

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

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

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

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

Исторический контекст

Обычная CRUD-модель хранит последнее состояние объекта и перезаписывает предыдущие значения. Она хорошо подходит для текущих операций, но сама по себе не отвечает на вопрос, какие изменения произошли, в каком порядке и каким было состояние заказа в прошлом.

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

Постановка проблемы

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

При неверном выборе можно получить две противоположные проблемы. CRUD-модель не даст надёжного исторического восстановления, а event sourcing без реальной необходимости создаст лишнюю сложность: медленное восстановление длинной истории, трудные изменения формата событий и необходимость поддерживать проекции.

Подробное решение

В event sourcing источником истины является журнал доменных событий. Событие описывает уже произошедший факт, например подтверждение заказа или добавление позиции, и после публикации не изменяется. Текущее состояние получают, применяя события в их логическом порядке.

Для восстановления состояния на дату берут только события с временем или последовательным номером не позднее требуемой точки. Важно различать время записи события и бизнес-время: для корректного воспроизведения обычно нужен устойчивый порядок в рамках конкретного заказа, а не только метка времени.

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

Текущие запросы обычно обслуживаются отдельной проекцией — таблицей или индексом, подготовленным под нужные сценарии чтения. Проекция может обновляться асинхронно, поэтому между записью события и появлением результата в чтении возможна задержка. Если бизнес требует немедленного чтения своего изменения, нужно явно обеспечить подходящую гарантию, например чтение после обработанного номера события.

Нужно заранее определить правила конкурентной записи. Для одного заказа полезен монотонный номер версии: запись принимается только при ожидаемой версии, а конфликт требует повторного чтения и принятия решения. Это предотвращает незаметное перетирание событий параллельными командами.

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

Event sourcing не равен простой публикации событий. При обычной CRUD-модели события могут быть вторичным уведомлением, тогда как в event sourcing именно журнал событий является каноническим состоянием. Кроме того, надёжная запись события и отправка сообщения во внешнюю шину требуют отдельного решения, например transactional outbox или встроенного журнала платформы хранения.

Следует учитывать приватность и юридическое удаление данных. Неизменяемый журнал может конфликтовать с требованиями удаления персональных данных, поэтому персональные значения не следует бездумно помещать в события. Возможные решения — минимизация данных, шифрование с удалением ключа, токенизация или отдельное хранение изменяемых персональных атрибутов; конкретный вариант зависит от требований регулирования.

Ситуация из практики

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

CRUD без полноценной истории был простым и быстрым, но не позволял надёжно восстановить состояние на произвольный момент. Отдельный аудит давал журнал действий, однако требовал поддерживать согласованность между изменением заказа и записью аудита и не гарантировал удобного воспроизведения состояния. Event sourcing обеспечивал точную последовательность фактов, но добавлял проекции, версионирование событий и операционную сложность.

Выбрали event sourcing для агрегата заказа, снимки после заданного количества событий и отдельную проекцию для рабочего интерфейса. Для редких исторических запросов состояние восстанавливалось из снимка и хвоста журнала, а обычные чтения шли из проекции. Решение было обосновано обязательной проверяемой историей, а не стремлением применить более сложную архитектуру.

Что кандидаты часто упускают

  1. Допустимо ли изменять уже сохранённое событие, если в нём обнаружена ошибка?

Нет, обычно событие не редактируют: это разрушает воспроизводимость истории и может сделать разные проекции несовместимыми. Исправление оформляют новым компенсирующим событием, например отменой ошибочного изменения, либо выполняют контролируемую миграцию версии события с полной проверкой всех проекций. Физическое исправление исходного журнала допустимо только как исключительная операция с понятным аудитом и процедурой восстановления.

  1. Что произойдёт, если проекция отстаёт от журнала событий?

Текущее чтение из проекции может быть временно устаревшим, но источник истины при этом не теряется. Обработчик должен быть идемпотентным, сохранять позицию обработки и уметь повторно обработать события после сбоя. Для критичных запросов можно читать журнал напрямую или требовать, чтобы проекция достигла определённой версии, но это увеличивает задержку и нагрузку.

  1. Когда event sourcing лучше не выбирать, даже если нужен аудит?

Если требуется только показать несколько полей «кто, когда и что изменил», обычная модель данных с корректным журналом аудита обычно проще и дешевле. Event sourcing оправдан, когда необходимо восстанавливать состояние, повторно проигрывать бизнес-логику, строить новые представления из полной истории или проверять порядок доменных фактов. Если эти свойства не нужны, сложность event sourcing не компенсируется преимуществами.