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

Для кого этот учебник?

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

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

  • Для людей вне сферы финтеха. Чтобы понять, как и почему разработка ПО для финансовых систем отличается от привычной вам.

Принципы

Вся изложенная в учебнике информация отвечает трём принципам:

  • Запрет на придуманные данные. Деньги не могут браться из воздуха, поэтому мы не можем мириться с дубликатами или произвольными обновлениями баланса. Реализуем мы это при помощи идемпотентности, дедупликации и выверки.

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

  • Запрет на доверие. Нельзя доверять ни внешним поставщикам, ни внутренним компонентам, ни миру. Этот принцип реализуется верификацией веб‑хуков, перекрёстной проверкой данных между источниками и громким объявлением о нарушенных допущениях.

Представление денег

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

Работа с точностью

Выбор представления денег — одно из самых фундаментальных решений в финансовых системах. Есть четыре основных варианта:

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

  • Произвольная точность. Типы наподобие BigDecimal языка Java позволяют чётко управлять точностью вычислений. Код предсказуем, и мы сами можем решать, где и как происходит округление. Этот вариант подходит для промежуточных работ, например, для обмена валют или расчёта цен, когда множество операций объединено в цепочку.

  • Точность в минорных единицах. Для большинства фиатных валют вполне нормально хранить только фиксированную точность, такую же, которая используется в привязанной к ней центральной банковской системе. Количество десятичных разрядов определяется ISO 4217 (не стоит предполагать, что оно всегда равно двум!). На практике это означает хранение в виде целого числа — €12,34 превращается в 1234. Для криптовалют используется тот же принцип целочисленных значений по наименьшей единице (satoshi для BTC, wei для ETH), но с двумя особенностями: точность задаётся для каждого актива и определяется самим токеном (например decimals ERC-20); часто она равна 18 разрядам, а получающиеся величины переполняют 64-битные integer, поэтому для их хранения нужны integer произвольной длины.

  • Рациональные числа. В случаях, когда потеря точности неприемлема. Это самое мощное решение, но оно обладает собственными особенностями. Во‑первых, оно медленнее остальных. Во‑вторых, его нельзя преобразовать в другие форматы без потери точности. В третьих, оно обычно требует специального типа данных или библиотеки.

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

С той же аккуратностью нужно подходить, если величины сериализуются. В большинстве парсеров number JSON считается double IEEE-754, поэтому сериализация денег в виде number снова приводит к пограничной проблеме чисел с плавающей запятой, как бы тщательно мы не задавали их внутреннее представление. Деньги следует передавать или в виде строки ("12.34"), или как целое число по минорным единицам.

Затронутые принципы:

  • Запрет на потерю данных. Неправильное представление приводит к незаметной потере точности, которую невозможно восстановить.

Стратегии округления

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

  • Это бизнес‑решение. Разные стратегии округления влекут за собой разные последствия. Иногда нужно подходить консервативно (например, не тратить того, чего у вас нет) и округлять вниз; иногда важно статистическое влияние и использовать округление до чётного. Решение о том, кому достанется дробная часть, может иметь юридические/налоговые последствия.

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

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

Затронутые принципы:

  • Запрет на потерю данных. Остатки нужно отслеживать, а не терять.

  • Запрет на придуманные данные. Округление никогда не должно генерировать несуществующие деньги.

Работа с валютами

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

  • Совместная упаковка величины и валюты. newtype (struct, class, record и так далее) Money минимизирует вероятность ошибок.

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

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

  • Коды идентифицируют только фиат. Коды валют уникальны и применимы в качестве идентификаторов только для фиатных денег. Для криптовалют придётся использовать более сложный подход, например, (network, contract address) или подобный ему.

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

  • Привязанные не равны исходным. Криптовалюты, привязанные к другим активам (pegged), перенесённые через блокчейн‑мосты (bridged) или представленные в виде обёрнутых токенов (wrapped), не являются полноценным эквивалентом исходных криптовалют.

Затронутые принципы:

  • Запрет на доверие. Валидировать валюту относительно контролируемого множества нужно на границе системы.

  • Запрет на придуманные данные. Если считать уникальные валюты/активы взаимозаменяемыми, то значения станут придуманными.

Курсы FX

Курсы FX (Forex, рынок обмена валют) позволяют нам конвертировать деньги между валютами.

  • Курс всегда имеет направление. Курс EUR к USD отличается от курса USD к EUR. На бирже покупка и продажа — это два разных ордера с разными ценами (ценами спроса и предложения, bid/ask), поэтому эти операции не взаимообратны.

  • Критично время курса. Хотя, строго говоря, можно использовать курс любого момента времени, чаще всего применяются следующие:

    • Текущий курс. Используется для расчёта текущих активов или стоимости транзакции, как если бы она происходила прямо сейчас.

    • Курс даты зачисления. Используется для расчёта изменения стоимости или величины налогов.

  • При конверсии важны два вида курсов:

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

    • Базовый курс (среднерыночный или центробанка). Он используется для вычисления оценки и эквивалентности (стоимость активов на данный момент или налоговая база на дату зачисления); это не цена, по которой торгуются валюты.

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

Затронутые принципы:

  • Запрет на потерю данных. Необходимо хранить величины (а для определения справочного курса обращаться к источнику).

  • Запрет на доверие. Каноничного курса нет, поэтому источник курса должен быть частью данных.

Фиксация денег: бухгалтерская книга

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

Двойная запись

Двойная запись — широко распространённый способ хранения финансовых транзакций в виде списка записей (credit account, debit account, amount) (это компактный вид; в классическом представлении на каждое перемещение используется отдельная строка дебита и кредита). Поскольку каждая запись перемещает одну и ту же сумму с одного счёта на другой, бухгалтерские книги всегда остаются сбалансированными — деньги лишь перемещаются, но не создаются и не исчезают.

  • У денег всегда есть источник и получатель. У внешних поставщиков тоже есть отдельные счета, поэтому деньги, попадающие в систему/исходящие из неё, всё равно отслеживаются.

  • Баланс никогда не хранится отдельно. Он вычисляется из перемещений денег.

  • У счетов есть типы: активы, задолженности или собственные средства. Поэтому всегда балансовое уравнение всегда справедливо (активы = задолженности + собственные средства), а у каждого счёта есть сторона, по которой его остаток увеличивается. На практике нам также нужны счета доходов и расходов, например, чтобы занести оплату в качестве прибыли или списание в качестве убытка (активы = задолженности + собственные средства + доход - расход).

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

  • Оприходованные записи неизменяемы. Изменения вносятся добавлением новых компенсирующих записей, корректирующих исходные.

Затронутые принципы:

  • Запрет на потерянные данные. Деньги могут только перемещаться между счетами; конечная сумма остаётся неизменной.

Момент валютирования, момент проводки и момент расчёта

У транзакций обычно есть как минимум две, иногда три временных метки:

  • Момент валютирования. Когда произошла транзакция.

  • Момент проводки. Время записи транзакции в системе.

  • Момент расчёта. Время реального перевода или материализации средств. Он есть не у каждой транзакции. Обычно выражается в виде T+X, где X — количество дней после валютирования, когда происходит расчёт (например, T+2 означает через 2 дня после валютирования).

Первые две метки почти всегда расходятся:

  • Запись задним числом (проводка > валютирование). Строго говоря, почти все транзакции записываются задним числом, но это наиболее важно, когда проводка и валютирование относятся к разным отчётным периодам, например, к дням, месяцам, годам.

  • Запись будущим числом (проводка < валютирование). Встречается реже, но случается, например, при запланированных платежах или с будущей датой исполнения — постоянное платёжное поручение, зарегистрированное сегодня, но действующее со следующей недели.

Пример: оплата картой произошла в T1 (момент валютирования), вы зафиксировали его в T2 (момент проводки), но платёжный сервис перевёл деньги на ваш счёт в T3 (момент расчёта).

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

Затронутые принципы:

  • Запрет на потерянные данные. Записываем все релевантные временные метки; объединение их в одну created_at приводит к потере информации, которую невозможно восстановить.

Аудиты и аудиторские следы

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

  • Не смешиваются ли средства компании со средствами пользователей и не используются ли средства пользователей для покрытия расходов компании?

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

  • Соответствует ли информация, предоставляемая внешним сторонам (например, пользователям или налоговым органам), реальному положению дел? Например, располагает ли компания таким объёмом активов, который соответствует её обязательствам перед пользователями?

  • Защищены ли средства от внешних угроз? (Например, кто и каким образом может получить к ним доступ?)

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

Чтобы быть полезным, аудиторский след должен включать отвечать на вопросы:

  • Что произошло.

  • Когда это произошло (см. разницу между временем валютирования и временем проводки).

  • Кто или что вызвало событие — пользователь, оператор или автоматизированная задача.

  • Почему оно произошло — ссылка на ордер, распоряжение или событие, которые стали его причиной.

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

Причина сама по себе нередко является результатом принятого решения (например, проверки на соответствие требованиям или оценки риска). Простого сохранения результата («заблокировано») обычно недостаточно для аудита, поскольку вас обязательно спросят, почему было принято именно такое решение. Если эта логика реализована в виде таблицы решений или механизма правил (DMN, Drools, Decisions4s), а не скрыта в императивном коде, то само решение становится структурированным артефактом, который можно воспроизвести. В нём фиксируется, какие правила были применены, к каким входным данным и к какому результату это привело.

Затронутые принципы:

  • Запрет на потерянные данные. Одно лишь текущее состояние не может ответить на вопросы аудита, это способна сделать только полная история.

Event sourcing

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

Практические примечания:

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

  • Вычисляемое состояние можно кэшировать. Для повышения производительности балансы и результаты расчётов можно хранить в кэше или периодически сохранять в виде снэпшотов.

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

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

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

Затронутые принципы:

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

Неизменность

Аудиторский след, который можно редактировать, ничего не доказывает, поэтому записи запрещается обновлять и удалять. Необходимо, чтобы в журнал можно было только добавлять записи, и каждое изменение должно быть новой записью (см. ниже).

Неизменяемость — это инвариант, и здесь применим обычный набор инструментов:

  • За счёт самой архитектуры системы. Использование таблиц только для добавления новых записей и запрет UPDATE/DELETE на уровне прав доступа к базе данных.

  • Проверки во время выполнения. Слой приложения не предоставляет операций изменения для уже проведённых записей.

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

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

Затронутые принципы:

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

Откаты и исправления

Ошибки всё равно случаются: например, может быть проведена неверная сумма или транзакция может попасть не на тот счёт. Неизменность означает исправление «вперёд» — нужно создать новую компенсирующую запись и связать её с записью, которую она исправляет, в обоих направлениях.

  • Откат. Полностью обращает исходное изменение, как будто оно никогда не происходило экономически; однако откат всё равно остаётся видимым в истории вместе с исходным изменением.

  • Исправление (корректировка). Либо отражает разницу между фактически записанным значением и правильным значением, либо выполняет обратную проводку и повторное проведение операции с верными данными.

  • Учитывайте отчётный период. Исправления часто попадают в другой отчётный период, чем исходная операция (см. различие между временем валютированием и временем проводки); именно связь между записями позволяет отчётам правильно их учитывать и отличать реальные операции от корректировок.

Последний момент особенно важен: при проведении корректировок или откатов операций нужно решить, следует ли датировать событие задним числом (указать время значения в прошлом) или нет. Здесь многое снова зависит от графика формирования отчётности — обычно нельзя задним числом отнести что‑либо к уже закрытому периоду, потому что информация за него уже была предоставлена внешнему миру.

Затронутые принципы:

  • Запрет на придуманные данные. Ошибки устраняются внесением связанных записей, компенсирующих исходную запись.

Неизменность и GDPR

Кажется, право на удаление по закону GDPR противоречит неизменности бухгалтерской книги. На практике решить эту проблему довольно просто:

  • Финансовые записи в значительной степени остаются исключением. Юридические требования по хранению данных (бухгалтерское законодательство, законодательство по противодействию отмыванию денег (AML), обычно предусматривающее хранение в течение 5–10 лет) имеют приоритет над запросами на удаление транзакционных данных. В течение этого срока бухгалтерские проводки не удаляются.

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

  • Криптографическое уничтожение данных (crypto‑shredding) для встроенных персональных данных. Если персональные данные необходимо включить в неизменяемые записи (например, в полезную нагрузку событий), персональные поля каждого пользователя следует шифровать отдельным ключом. Тогда удаление данных сводится к уничтожению этого ключа, а не к переписыванию истории.

Затронутые принципы:

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

Реализация денежных потоков

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

Инварианты

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

Существует три основных способа обеспечения инвариантов:

  • За счёт самой архитектуры системы. Систему можно спроектировать так, чтобы она позволяла создавать только корректные объекты, делая невозможным представление недопустимых состояний. Для этого можно использовать различные подходы: фабричные методы (умные конструкторы), программирование на уровне типов (например, уточнённые типы, refined types), ограничения базы данных.

  • Проверки во время выполнения. Проверка соблюдения инвариантов непосредственно при выполнении логики. Это могут быть как утверждения в коде продакшена, так и тесты. Особенно хорошо здесь подходит тестирование на основе свойств, например: «для любой последовательности проводок бухгалтерская книга остаётся сбалансированной».

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

Важно понимать, что эти методы дополняют друг друга, и для достижения требуемого уровня надёжности обычно используются все три одновременно. Архитектурное решение — самое надёжное, но с его помощью невозможно выразить всё (особенно инварианты, охватывающие несколько совокупных величин или даже несколько систем). Проверки во время выполнения позволяют обнаруживать нарушения в момент их возникновения, а проверки постфактум — единственный способ выявить ошибки, уже попавшие в рабочую систему, хотя обнаруживаются они уже после.

Затронутые принципы:

  • Запрет на доверие. Инварианты верифицируются, а не принимаются на веру; проверяется даже ваш собственный код.

Резервирование средств

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

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

Для решения этой проблемы используют резервирование средств (также известное как hold‑and‑release). Сначала средства резервируются под конкретную транзакцию, и лишь затем начинается взаимодействие с внешней системой. После его успешного завершения резервирование закрывается, и транзакция выполняется; если же что‑то пошло не так, резервирование снимается, а средства снова становятся доступны на счёте.

Этот шаблон вводит различие между двумя видами баланса: общим балансом (total balance), включающим все средства пользователя, в том числе зарезервированные, и доступным балансом (available balance); доступный = общий - зарезервированный.

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

Пара практических замечаний:

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

  • Любое резервирование должно быть завершено. Резерв, который не был ни использован при расчёте, ни снят, блокирует средства пользователя. Поэтому любой процесс, создающий резервирование, должен гарантировать, что рано или поздно оно будет завершено. В качестве дополнительной страховки можно использовать явный срок действия или тайм‑аут, хотя это необязательно — можно полагаться и на внутреннюю дисциплину системы. Примечательно, что такой отказ консервативен: «осиротевшее» резервирование лишь блокирует деньги, но не приводит ни к их потере, ни к их созданию.

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

Затронутые принципы:

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

Работа с овердрафтами

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

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

  • Непреднамеренный. Баланс становится негативным, хотя политика системы это запрещает.

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

«Запрещено» не значит «непредставимо». Возникает соблазн зафиксировать правило «баланс никогда не бывает отрицательным» на уровне типов или хранения данных, например, используя беззнаковое целое число или ограничение CHECK (balance >= 0). Однако если система вынуждена принять отрицательный баланс, невозможность представить такое состояние приведёт к тому, что у неё произойдёт сбой в процессе выполнения операции, либо она незаметно «обрежет» баланс до нуля (тем самым создав деньги из воздуха), либо совершит какую‑нибудь другую столь же некорректную ошибку.

Иными словами, правило balance >= 0 — это всего лишь инвариант, поэтому к нему применим обычный набор инструментов. Обеспечивайте его соблюдение во время выполнения при авторизации транзакций, выявляйте нарушения постфактум с помощью мониторинга и сверки, но не пытайтесь навязать его за счёт самой архитектуры системы. Если обнаружен овердрафт, это сигнал для расследования, но не обязательно признак ошибки.

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

Затронутые принципы:

  • Запрет на придуманные данные. Преобразование отрицательного баланса в нулевой приводит к созданию денег из воздуха.

  • Запрет на доверие. Внешний мир может принудить к овердрафту, что бы ни говорили ваши проверки.

Идемпотентность

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

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

  • Выберите способ воспроизведения ошибок. Должен ли первый сбой вызова приводить к повтору сохранённой ошибки или к повторному запуску обработки? Обычно проще воспринимать идемпотентным результатом ошибку и воспроизводить её. Клиент всегда может совершить повторную попытку с новым ключом. Многое зависит от природы ошибок — постоянные (например, валидации) должны воспроизводиться в том же виде, а временные (например, сетевой сбой) должны обрабатываться заново.

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

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

  • Учитывайте временные окна. Возможно, вам захочется использовать временное окно идемпотентности, например, выполнять дедупликацию только в пределах 24 часов. Это существенно упрощает реализацию (в противном случае объём данных растёт бесконечно), но за это приходится расплачиваться корректностью. Идите на этот компромисс только в случае крайней необходимости, потому что в дальнейшем он вам аукнется.

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

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

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

Затронутые принципы:

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

Полная возобновляемость

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

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

  • Что‑то должно возобновлять остановленное движение. Независимый движущий механизм (планировщик, воркер или алгоритм опроса) должен подхватывать незавершённые операции движения средств и двигать их дальше. Движение не должно прекращаться навечно в случае аварийного сбоя оркестратора.

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

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

Можно использовать движок надёжного исполнения (например, Temporal, Camunda, Workflows4s, AWS Step Functions) или вручную развернуть собственный конечный автомат с сохранением состояния в хранилище.

Затронутые принципы:

  • Запрет на потерю данных. Сбой посередине процесса никогда не должен приводить к потере перемещаемых средств; для продолжения и завершения необходимо сохранение прогресса в постоянное хранилище.

  • Запрет на придуманные данные. Возобновление повторно выполняет шаги, поэтому они должны применяться повторно без двойного учёта, благодаря чему процесс выполняется ровно один раз.

Продолжение следует

Комментарии (2)


  1. itGuevara
    27.08.2026 19:07

    Хотелось бы: подборку ссылок на подобное, формат ТОП-20 табличек банка (остатки \ баланс, проводки, клиенты и т.п.), каким типом данных задавать 20-значный номер расчетного \ лицевого счета (не числовой же и с подсветкой составляющих) и т.п.
    Также хотелось бы обзор банковских онтологий (FIBO etc). Тогда описание транзакции и остатка так будут:

    @prefix ex: <http://example.com> .
    @prefix xsd: <http://w3.org> .
    
    # Описание проводки (транзакции)
    ex:Transaction_12345 a ex:AccountingEntry ;
        ex:hasDebitAccount ex:Account_40702... ;
        ex:hasCreditAccount ex:Account_40817... ;
        ex:hasAmount "5000.00"^^xsd:decimal ;
        ex:hasCurrency "RUB" ;
        ex:bookingDate "2026-08-27T14:30:00Z"^^xsd:dateTime .
    
    # Описание остатка на счете
    ex:Balance_Account_40702 a ex:AccountBalance ;
        ex:appliesToAccount ex:Account_40702... ;
        ex:hasAmount "125000.50"^^xsd:decimal ;
        ex:asOfDate "2026-08-27T23:59:59Z"^^xsd:dateTime .
    

    Да, а хабру нужно в список языков ("вставить код") добавить "RDF turtle".


    1. ThinkFresh
      27.08.2026 19:07

      каким типом данных задавать 20-значный номер расчетного

      Не знаю как у всех, но у нас - стринга обычная)