АрхитектураМикросервисы и интеграцииАрхитектор интеграционных решений

В контракте события сумма передаётся как число с плавающей точкой. Какой дефект интеграции может привести к...

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

{
  "type": "PaymentCaptured",
  "paymentId": "p-42",
  "amount": 10.10,
  "currency": "EUR"
}
Проходите собеседования с ИИ помощником Hintsage

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

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

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

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

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

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

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

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

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

Один из надёжных вариантов — передавать сумму как целое число минимальных единиц:

{ "type": "PaymentCaptured", "paymentId": "p-42", "amountMinor": 1010, "currency": "EUR" }

В этом случае 1010 означает 1010 центов для EUR. Но одного целого числа недостаточно: контракт должен определить валюту, а также правила для валют с другой дробностью или без дробной части. Нельзя безоговорочно считать, что любая валюта имеет ровно две десятичные позиции.

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

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

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

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

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

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

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

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

Нет. Число 1010 без валюты не определяет, означает ли оно 10,10 евро, 1010 японских иен или другую сумму. Валюта также определяет допустимую дробность и правила отображения, поэтому она является частью денежного контракта.

  1. Где должно выполняться округление — у отправителя или у каждого потребителя?

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

  1. Можно ли использовать двоичный тип с фиксированной точностью вместо целого числа?

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