АналитикаСистемный анализСистемный аналитик

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

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

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

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

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

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

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

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

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

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

Особенно опасны частичные сбои: позиция уже обновлена, а итоговая сумма — нет; либо итог пересчитан по устаревшей версии позиций. Это влияет на оплату, лимиты, отчётность и любые решения, принимаемые по итоговой сумме.

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

Сначала нужно явно разделить поля на источники истины и производные данные. Например, позиции заказа, зафиксированные цены, правила скидок и налоги могут быть источниками, а сумма без скидки, скидка и итог — вычисляемыми результатами.

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

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

Иногда допустима eventual consistency: исходные данные фиксируются сразу, а итог пересчитывается обработчиком события. Тогда интерфейс и бизнес-правила должны учитывать временную неактуальность, а система — иметь повторную обработку, идемпотентность и периодическую сверку.

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

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

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

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

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

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

  1. Можно ли считать итоговую сумму источником истины, если она уже сохранена в заказе?

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

  1. Достаточно ли одной транзакции, если итог обновляется отдельным сервисом?

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

  1. Что делать, если пересчёт производного значения завершился ошибкой?

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