АрхитектураАрхитектура ПОАрхитектор программного обеспечения

Допустим, текущее состояние агрегата восстанавливают проигрыванием событий, но правила расчёта изменились. ...

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

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

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

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

Снимки состояния ускоряют восстановление, но сами по себе не решают проблему изменения семантики. Корректность требует явно определить, должно ли старое состояние отражать исторические правила или пересчитываться по новым.

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

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

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

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

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

Это создаёт несколько рисков:

  • сумма исторического заказа перестаёт совпадать с выставленным счётом;
  • отчёты и аудит начинают зависеть от текущей версии кода;
  • повторное воспроизведение одной истории даёт разные результаты после релиза;
  • ошибка в обработчике может повредить все агрегаты, восстановленные из событий.

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

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

События делают неизменяемыми. Если смысл факта изменился, существующее событие не переписывают: добавляют новую версию события либо вводят явное преобразование старого формата в новый.

Есть несколько связанных механизмов.

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

  2. Сохранение исторической семантики. Если событие означает «скидка составила 10 процентов», обработчик должен восстановить именно этот факт. Нельзя при replay заново вычислять скидку по текущей политике, если результат вычисления не был частью события.

  3. Upcasting. Старое событие преобразуют в актуальную форму без изменения его бизнес-смысла. Это удобно при изменении структуры данных, но опасно, если преобразование пытается задним числом применить новую бизнес-политику.

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

  5. Снимки. Периодически сохраняют состояние агрегата после некоторого события и проигрывают только хвост истории. Снимок должен содержать версию модели и иметь понятную стратегию недействительности: после изменения правил его может потребоваться пересоздать или явно признать совместимым.

  6. Разделение восстановления и пересчёта. Историческое восстановление отвечает на вопрос «каким было состояние тогда», а миграция отвечает на вопрос «каким оно должно стать по новым правилам». Пересчёт нельзя незаметно смешивать с обычным replay.

Компромисс состоит в стоимости. Чем больше версий событий и обработчиков поддерживается, тем сложнее код и тестирование. Полная миграция всей истории упрощает runtime, но требует ресурсов, контроля идемпотентности и возможности отката; хранение нескольких версий уменьшает миграционный риск, но продлевает архитектурный долг.

Для проверки используют детерминированный replay-тест: одна и та же история должна давать ожидаемый результат на каждой поддерживаемой версии модели. Также проверяют, что новый релиз не изменяет смысл уже подтверждённых финансовых и аудиторских фактов.

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

Платёжная система хранила события операций. После изменения правил округления повторное воспроизведение старых платежей стало давать сумму на несколько копеек меньше исходной. Команда рассмотрела три варианта.

Первый вариант — переписать все старые события с учётом новой формулы. Он кажется простым, но уничтожает историческую достоверность и затрудняет аудит. Второй — всегда применять текущую формулу при replay. Это не требует миграции, однако одно и то же событие начинает означать разное в разные моменты времени.

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

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

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

  1. Нужно ли версионировать только схему события?

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

  1. Можно ли решить проблему простым пересозданием снимков?

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

  1. Когда допустимо пересчитать старые состояния по новым правилам?

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