フィンテック・エンジニアリング・ハンドブック
(w.pitula.me)- お金を中核状態として扱うシステムは、データを作り出さず、失わず、何も信頼しないという原則の上で設計されなければならない
- 金額表現では float を避け、
BigDecimal、最小単位の整数、有理数などを責務に応じて組み合わせるべきであり、JSON の数値シリアライズも IEEE-754 double の問題を再び生む可能性がある - 台帳は 複式簿記、不変の監査証跡、value time・booking time・settlement time の分離、訂正・取消の記録を通じて、残高とレポートを再構成できる状態を保つ
- 実際のお金の流れでは、予約、冪等性、再開可能な状態機械、外部 API・Webhook の検証、outbox・CDC、照合(reconciliation) によって重複支出と欠落を防がなければならない
- アクセス制御、four-eyes 承認、SDLC の変更追跡、プロパティベーステストと障害注入は、内部オペレーターやコード変更までも 信頼境界 として扱うようにする
フィンテックシステムの基本原則
- お金がシステムの主たる関心事であるソフトウェアエンジニアリングでは、一般的な CRUD よりも追跡可能性、不変性、検証可能性のほうがはるかに重要である
- 対象読者は、フィンテックに新しく加わった人、すでにフィンテックで働いている人、フィンテックの外でお金のシステムが一般的なシステムとどう違うのかを理解したい人である
- すべてのパターンは、3つの原則を守るための手段である
- No invented data: お金は存在しなかった場所から作り出されてはならないため、重複処理や任意の残高変更を許さない
- No lost data: お金に起きたすべての出来事は追跡され、永続化されなければならない
- No trust: 外部プロバイダー、内部コンポーネント、現実世界を信頼せず、検証する
お金を表現する方法
- 金額表現は金融システムの最も基本的な決定のひとつであり、誤ると上位レイヤー全体がそのエラーを引き継ぐ
- float/double は予測しにくい精度損失を生みうるため、ほぼ常に良い選択ではない
- 高速でメモリ効率が良く、追加のライブラリやデータ構造を必要としないという利点はある
BigDecimalのような 任意精度 型では、計算精度と丸め位置を明示的に制御できる- FX や価格計算のように複数の演算が続く中間計算に適している
- 最小単位の整数 で保存する方法は、ほとんどの法定通貨において中央銀行システムと同じ固定精度を使う方式である
- €12.34 は
1234として保存される - ISO 4217 の桁数に従うべきであり、常に 2 桁だと仮定してはならない
- 暗号資産も satoshi や wei のような最小単位整数を使うが、精度は資産ごとに異なり、ERC-20 の
decimalsのようにトークンが定義する - 暗号資産の金額は 64 ビット整数を超えることがあり、任意幅整数が必要になる場合がある
- €12.34 は
- 有理数 は精度損失が許されない場合に最も強力だが低速であり、他の形式へ変換する際に精度損失なしで変換しにくく、通常はカスタム型やライブラリが必要になる
- 保存方式と計算方式は別個の決定であり、ひとつのシステムが整数保存と
BigDecimalの中間計算を併用することもできる - 金額のシリアライズでも境界の扱いが重要である
- 一般的な JSON 数値はほとんどのパーサーで IEEE-754 double であるため、内部表現に注意していても境界で float の問題が再発する
- お金は
"12.34"のような文字列か、最小単位の整数で送るべきである
丸めと通貨処理
- 丸め は、除算、通貨換算、手数料、利息、比率の適用、精度の移動で不可避であるため、暗黙にしてはならない
- 丸め戦略はビジネス上の決定である
- 保守的に切り下げる必要がある場合もあれば、統計的効果のために half-even を使うこともある
- 端数の残りを誰が受け取るかは、法務・税務上の影響を持ちうる
- 可能な限り長く完全な精度を維持し、通常は保存前やユーザー表示前のような境界でのみ丸めるべきである
- 値を複数の部分に分けたあとで丸めると、部分和が元の値と異なることがある
- 状況によっては明示的な rounding account が必要になる
- お金は数値だけでは表現できず、常に通貨と一緒に扱わなければならない
Moneyのような newtype、struct、class、record で金額と通貨をまとめると、エラーの可能性を減らせる- 異なる通貨どうしの加算は禁止し、換算は厳格に管理された為替レートで明示的に行うべきである
- 任意の通貨コードを受け入れず、システム境界で管理された通貨集合に対して検証しなければならない
- 法定通貨コードは識別子として使えるが、暗号資産には
(network, contract address)のような、より複雑な識別が必要である - pegged、bridged、wrapped の暗号資産は基礎資産と同等ではない
FX 為替レート
- FX rate は常に方向性を持つ
- EUR/USD レートは USD/EUR の単純な逆数ではない
- 取引所では買いと売りは bid/ask spread のため互いに異なる価格の注文である
- レートの時点も結果を変える
- 現在時点のレートは、現在の保有分や今発生したと仮定した取引価値を計算するために使われる
- value-date レートは、価値変動や税額計算に使われる
- 変換では 2 種類のレートが重要である
- Transactional rate は実際に変換が行われたレートであり、元の金額と結果金額から導かれる
- Reference rate は保有分の価値や税務基準のような評価と同等性判断に使われるもので、実際の取引価格ではない
- 標準的な単一の為替レートは存在しない
- レートは市場で形成され、取引場所や計算方式によって異なる
- 中央銀行レートは標準に最も近いが reference rate としてしか使えず、代替ソースも有効でありうる
- 後から検証できるように、金額と reference rate の出所を一緒に保存しておく必要がある
台帳と複式簿記
- お金の移動は、監査可能で、何年後でも再構成できる方法で記録されなければならない
- 複式簿記 は、金融取引を
(credit account, debit account, amount)形式の entry 一覧として保存する広く使われる方式である- 古典的な表現では、移動ごとに debit row と credit row を別々に置く
- すべての entry が同じ金額をひとつの勘定から別の勘定へ移すため、台帳は常にバランスする
- お金には常に出所と行き先がある
- 外部プロバイダーにも専用勘定を持たせることで、システムに出入りするお金を追跡できる
- 残高は保存せず、お金の移動から導出する
- 勘定には asset、liability、equity のようなタイプがある
- accounting equation
assets = liabilities + equityが維持される - 実際には、手数料収益や write-off 損失を記録するために revenue 勘定と expense 勘定も必要である
- 拡張式は
assets = liabilities + equity + revenue - expensesである
- accounting equation
- 1 つの取引は通常、複数の移動を生む
- 純額の移動と手数料の移動が別々に発生することがある
- posted entry は慣例上不変であり、訂正は元の記録を相殺する新しい entry を追加して処理する
時間モデル: value, booking, settlement
- 取引には通常 2 つ以上、ときには 3 つのタイムスタンプが付く
- Value time: 取引が実際に発生した時点
- Booking time: システムに記録された時点
- Settlement time: お金が実際に移転または実現した時点
- Settlement time はすべての取引に存在するわけではなく、通常は T+X で表される
- T+2 は value date から 2 日後に settlement が起きることを意味する
- value time と booking time はほぼ常にずれる
- booking > value なら backdated であり、レポート期間が変わるときに特に重要である
- booking < value なら forward-dated であり、予約決済や将来日付の決済で発生する
- カード決済の例では、T1 に決済が発生し、T2 にシステムが記録し、T3 に決済プロバイダーが口座へ資金を移す流れになる
- ビジネスレポートは主に value time または settlement time を見て、booking time は追跡可能性に有用である
- 複数の時点を
created_atひとつにまとめると、後で再構成できない情報を失う
監査証跡、イベントソーシング、不変性
- 金融システムは規制監査の対象であり、ユーザー資金と会社資金が混在していないこと、収益を説明可能であること、外部に提供した情報と現実が一致していること、資金が保護された状態にあることなどを証明しなければならない場合がある
- audit trail は、現在の状態だけでなく、その状態がどのように作られたかの全履歴である
- 何が起きたか
- いつ起きたか
- 誰または何がトリガーしたか
- なぜ起きたか
- 資金移動だけでなく、手動介入、fee schedule、rate source、limit といった設定変更、権限変更についても監査証跡が必要である
- compliance check や risk score のような判断は、結果だけ保存しても不十分な場合がある
- DMN、Drools、Decisions4s のような decision table や rules engine 上にあれば、どのルールがどの入力で実行され、どの結果になったかを再現可能な構造になる
- Event sourcing は audit trail を作るための体系的なアプローチである
- 現在の状態とログを別々に保存するのではなく、イベントだけを保存し、その後で状態を導出する
- 複式簿記の ledger は、balance を保存せず entry から計算するという点で、このパターンの一例である
- Event sourcing には実務上の制約も大きい
- どこにでも必要というわけではなく、ledger が資金をすでにカバーしていれば、周辺ドメインは通常のモデルと信頼できる change log で十分な場合がある
- パフォーマンスのために、balance や projection をキャッシュしたり snapshot したりできる
- イベントの原本は効率的にクエリしにくく、projection の作業が多くなることがある
- イベントは何年にもわたって残るため、今日のコードがはるか昔のイベントも読めなければならない
- 監査証跡は変更可能だと証拠にならないため、append-only でなければならない
- append-only table、DB 権限での
UPDATE・DELETEの除去、アプリケーション層での mutating operation の遮断、checksum や hash chain による tamper evidence などが手段になる
- append-only table、DB 権限での
- 実際のシステムでは、バグのために event log や audit trail を修正しなければならないことがある
- 通常、データが外部に報告された後は固定されるべきであり、報告前であれば、問題を見つけてシステム外に出る前にその場で修正できる場合がある
取り消しと訂正
- 誤った金額の posting や誤った口座への posting のようなミスは起こりうる
- 不変性は、将来に向かって修正する方式を要求する
- 新しい compensating entry を posting し、元の record と双方向に関連付ける
- Reversal は元の処理を経済的には存在しなかったかのように完全に相殺するが、元の処理と reversal はどちらも履歴に残る
- Correction または adjustment は、実際の記録と正しい値との差分を booking するか、いったん取り消したうえで正しい値で再度 posting する
- 訂正は元の処理とは異なる報告期間に入ることがある
- 関連付け情報が必要であり、そうして初めてレポートが訂正を正しく帰属させ、実際の活動と cleanup を区別できる
- すでに締められた報告期間への backdate は通常許可されないため、訂正時に過去の value time を指定するかどうかは報告スケジュールに左右される
資金フローの実行: 不変条件と資金予約
- Invariant は、システム内で常に真でなければならない性質である
- accounting equation はその一例であり、ビジネス上の利害関係者が複数の条件を定義できる
- invariant を強制する方法は相互補完的である
- 生成段階で有効なオブジェクトだけを作るよう設計する
- ランタイムで assertion、テスト、property-based testing によって確認する
- 保存済みデータを reconciliation job や nightly check で事後分析する
- 外部世界と相互作用する取引では、race condition を避けなければならない
- 外部呼び出しの後になって残高不足が判明したり、同じ資金を二重に使ったりする状況を防ぐ必要がある
- Funds reservation または hold-and-release は、外部との相互作用の前に特定の取引のために資金を予約するパターンである
- 成功したら reservation を settle して取引を進める
- 失敗したら release して available balance に戻す
- このパターンでは total balance と available balance を区別する
available = total - reserved- 残高確認と新しい reservation は available balance を基準に実行される
- 最終金額は事前見積もりと異なる場合がある
- 手数料や為替レートが異なれば、見込み金額を予約し、実際の金額を settle したあと、残りを release する
- reservation は必ず settle または release されなければならない
- 孤立した reservation はユーザー資金をロックするが、資金を失わせたり生み出したりはしない
- expiry や timeout はセーフティネットになりうるが、必須ではない
- 残高確認と reservation の記録は linearizable でなければならない
- stale read では、2つの取引がどちらも確認を通過し、同じ資金を裏付けにしてしまう可能性がある
Overdraft と冪等性
- Overdraft は口座残高がマイナスになる状況である
- 意図的な overdraft は、限度額と利息を持つ信用商品であり、通常は別個の overdraft 口座としてモデル化される
- 意図しない overdraft は、ポリシー上禁止されていても発生しうる
- settlement が予約見積もりより大きく入ったり、reversal が資金流出後に入ったりすることがある
- funds reservation はそのウィンドウを小さくするが、なくすことはできない
- 「禁止」と「表現不可能」は異なる
- unsigned integer や
CHECK (balance >= 0)で負の残高を表現できないようにすると、現実に負の残高を受け入れなければならないときに crash、0 への clamp、誤処理につながる可能性がある - 負の残高を 0 に clamp すると、資金を生み出してしまう
- unsigned integer や
- overdraft が検知されたら、調査のシグナルとして扱い、将来の入金との netting、返済要求、expense/loss 口座への write-off などの方法で明示的に回収または処理しなければならない
- 分散システムでは exactly-once delivery を保証できないため、リトライが必要であり、リトライは重複配送を生む可能性がある
- Idempotency は、同じメッセージが2回配送されても処理が1回分の効果しか持たない性質である
- 明示的な idempotency key は、payload ベースの deduplication より通常は単純で安全である
- key は特定の operation と client のスコープに限定すべきである
- エラーを再生するか再処理するかを決める必要があり、恒久的なエラーはそのまま再生するほうが通常は単純である
- 重複呼び出しの payload が元と同じかを検証するのは良い実践だが、実装の複雑さと柔軟性のコストがある
- 大規模では、数十億件のリクエストの deduplication と同時アクセスに対する atomic barrier が必要になる
- 24時間のような idempotency window は実装を単純化するが、correctness のコストが大きい
- retry のテストと out-of-order retry の処理も必要である
再開可能なフロー
- 資金フローは複数段階にまたがるため、段階の間ならどこでも停止しうると想定しなければならない
- Full resumability は、中途半端に終わったフローが常に復旧可能な状態に置かれるようにする設計である
- 進行状態はメモリではなく永続ストレージに保存しなければならない
- フローを明示的な state machine としてモデル化し、各段階の完了を次の段階の開始前に commit する
- 中断されたフローを再び前に進める独立した driver が必要である
- scheduler、worker、poller は orchestrator の crash 後でも incomplete flow を処理できなければならない
- 再開時には、すでに部分的に起きた段階を再実行することがありうるため、各段階は冪等でなければならない
- 外部効果は rollback できない
- 外部世界を呼び出した後は、呼び出していない状態には戻せない
- 完了するまで roll forward するか、その後の段階が恒久的に失敗したなら、saga pattern のように compensating action を posting しなければならない
- Temporal、Camunda、Workflows4s、AWS Step Functions のような durable-execution engine を使うか、自前で persistent state machine を作ることができる
外部APIの利用
- 決済プロバイダー、custodian、blockchain node、KYC vendor などの外部APIは、コード、品質、稼働時間を制御できないため、防御的に扱うべきである
- スキーマを信頼してはいけない
- フィールド欠落、型変更、想定外の null が起こりうる
- 重要な部分は境界で検証し、想定外のデータは大きく失敗させるべきである
- 不要な部分まで検証すると、第三者の契約違反のために不要な障害が発生することがある
- 外部APIでは、URL での token 受け渡し、精度損失、意味と異なる HTTP code、
200の中の error body、一貫しない pagination、custom date format のようなことが十分に起こりうる - すべての呼び出しは失敗しうるため、timeout と retry が必要である
- Circuit breaker は主に過負荷のサーバーに対する courtesy であり、クライアントの複雑さを増す
- ただし、latency、thread、connection のような有限資源を保護する際には必要になりうる
- rate limit と quota は、事前計算によって想定呼び出し量と provider limit を照らし合わせる必要がある
- すべての request と response を構造化され、問い合わせ可能な形で保存すれば、調査、audit trail、provider の挙動に関する紛争の証拠、バグ後の再処理資料になる
- 中核領域では provider redundancy を検討できる
- 2つの blockchain node によるデータ検証、backup bank partner、crypto custodian、KYC vendor といった方法がある
- 開発、手数料、複雑性のコストは非常に大きい
- sandbox は production と大きく異なることがあるため、canary release や影響の小さい controlled usage によって production test を準備すべきである
Webhook 処理
- Webhook は外部システムからの信号を受け取る一般的な方法だが、安全に処理するのは容易ではない
- 順序を前提にしてはいけない
- メッセージは out-of-order で届いたり、stale data を含むことがある
- たった今受け取った Webhook を最新の真実とみなして状態を上書きしてはいけない
- 有効性を前提にしてはいけない
- Webhook は issuer の別の下位システムから送られ、stale または誤変換されたデータを含むことがある
- Webhook 本文は trigger としてのみ使い、API を照会して authoritative state を確認するやり方が望ましい
- API も eventually consistent である可能性があるため、直後に照会すると以前の状態を返すことがあり、retry が必要である
- 配信を前提にしてはいけない
- issuer が強い redelivery policy を約束していても、Webhook はいつか失われる
- reconciliation のような独立したプロセスがデータ完全性を補完しなければならない
- 単一配信も前提にしてはいけない
- 同じ Webhook は複数回配信され、処理は冪等でなければならない
- すばやく acknowledge し、非同期で処理すべきである
- raw event を durable store に保存したあと、ただちに 2xx を返し、実際の作業は非同期で実行する
- raw payload はそのまま保存すべきである
- 安定した処理、audit trail、バグ後の再処理に必要である
- 呼び出し元は検証しなければならない
- 一般に issuer は payload signature を付け、受信者は shared secret の HMAC または公開鍵ベースの非対称署名で検証する
- 署名検証は再シリアライズした payload ではなく、受信した raw bytes に対して行うべきである
- Webhook は、何が起きたかという真実ではなく、何かが起きたという hint として扱うべきである
信頼できる通知: Outbox と CDC
- システム変更を Kafka event、webhook call などの外部チャネルで確実に通知する必要があるとき、transactionality が問題になる
- publish は成功したがネットワーク問題で応答を受け取れずシステム状態を rollback したり、state change は commit されたが publish が失敗したりする状況が起こりうる
- textbook な答えは 2-phase commit または distributed transaction だが、複雑性と再利用の標準化の難しさのため、まれにしか使われない
- 実用的な選択肢はいくつかある
- Outbox pattern: 状態変更とともに publishing intent を専用 store に transactionally 記録し、あとで成功するまで処理する
- Change Data Capture: database の write-ahead または replication log を読み、commit 済みの変更を event stream に変換する
- Debezium と AWS DMS が CDC を提供する
- CDC は table row 形式の raw event を出力するため、内部 schema の漏出を避けるには postprocessing が必要である
- Listen-to-yourself は、まず event を publish し、その event から自分の状態を再構築する
- Event sourcing は event log がすでに DB にあるため、そこから publish すればよい
- どのメカニズムを選んでも delivery は at-least-once である
- relay や connector が publish 後、記録前に crash すると、再起動時に再送されることがある
- consumer は安定した event id で deduplicate し、冪等に動作しなければならない
Reconciliation
- 外部データに依存するシステムは、2つのシステムの状態がずれる data drift に脆弱である
- Webhook を取り逃したり、ledger では transaction が posting されたのに external provider system には反映されていないことがありうる
- Reconciliation は2つのシステムを突き合わせるプロセスである
- 実際には ledger、payment processor、bank のように3つ以上であることもある
- cadence は文脈と制約に応じて hourly、daily、monthly、yearly になりうる
- drift は missing data の場合もあれば、同じ transaction で金額が異なるような、より複雑な差異である場合もある
- timing も重要である
- settlement が T+3 であれば、record は3日間 unreconciled 状態でありうるため、不要な alert を防ぐにはこれを process に反映しなければならない
- matching algorithm が中核的な難所である
- 通常は external provider id を内部に保存すれば matching は単純になる
- それがなければ amount と time に基づく heuristic が必要になることがある
- one-to-many reconciliation も必要である
- 1つの settlement transfer が複数の transaction を包含することがある
- discrepancy を reconciliation が合うように単純に overwrite してはいけない
- correction record、Webhook data の再処理のような first-class support によって原因を理解し、修正しなければならない
制御とアクセス
- 金銭システムでは、データだけでなく、誰がどの行動を取れるかも制御し、事後に手続き順守を証明できなければならない
- Segregation of duties は、1人がプロセス全体を所有できないようにする統制である
- Four-eyes、maker-checker、dual control は、特定の action が適用される前に、2人目の承認を必要とする方式である
- 対象となるのは、大規模または手動の withdrawal、manual ledger correction、treasury や cold-wallet の移動、fee schedule や limit の変更のように、資金を動かしたり誤表示したりしうる行動である
- エンジニアリングにも同じ統制が適用される
- code merge、production deploy、infrastructure change は、金銭システムでは機微な action であるため、review と approval が必要である
- approval 自体も trail の一部である
- 誰が要求し、誰が承認し、その2人が別人だったかを記録しなければ、control を証明できない
- emergency に備えて、明示的で、強く監査される break-glass 経路が必要である
- Access control はシステム状態の一部であり、時間とともに変化する
- human と service の両方に最小権限を与えるべきである
- 個人単位の grant より RBAC を優先すると review しやすい
- capability の grant と revoke も機微な event であるため、何が、誰により、なぜ変更されたのかを記録しなければならない
- scheduled access review によって、古くなった、または不正確になった permission drift を捉えるべきである
SDLCの変更追跡
- 規制環境では、コードがproductionに到達する過程を監査できなければならない
- source controlは変更の記録である
- commit historyはすべての変更をauthorに帰属させ、reviewとlinked ticketを通じて理由と結び付ける
- signed commit、protected branch、shared historyへのforce-push禁止で保護すべきである
- reviewとpipelineは強制されるべきである
- required review、status check、main branchへのdirect push禁止が重要である
- deploymentは追跡可能でなければならない
- どのversionが稼働中なのか、誰がいつreleaseしたのかを再構成できてはじめて、incidentを原因となった変更と結び付けられる
テスト戦略
- お金を扱うシステムではoperation sequenceの空間が広く、興味深い失敗は組み合わせから生まれるため、テストが特に重要である
- Property-based testing は、特定の出力ではなく、どの入力でも成り立つべきpropertyを検証する
- invariantとmoney mathに適している
- operation sequenceを生成するときは、最後だけでなく各ステップの後でもinvariantを確認しなければならない
- 手動で大規模に実行するのは難しいため、assertionを自動注入するtesting harnessが必要である
- Generative idempotency testingは、外部世界に触れるすべてのoperationが2回目の呼び出しで影響を持たないことを検証する
- Crash and resume injectionは、long flowがどのステップ間で停止しても復旧できるかを検証する
- Round-trip testingは、encode/decode、serialize/deserialize、convert/convert backの後に出発点へ戻るか、または既知のtolerance内に収まるかを確認する
- moneyとcurrency typeの境界での精度損失やserialization bugを捉えるための迅速な方法である
- Golden testingは、fee breakdown、statement、reportのような計算結果を保存済みの期待結果と比較し、意図しないdiffを明らかにする
- Backward-compatibility testingは、古いreal-format payload corpusを維持し、現在のコードが依然としてdeserializeとprojectを正しく行えるかを検証する
- Production testingは、sandboxがproductionと大きく異なる場合に必要になることがある
- canary release、blast radiusを小さく抑えたcontrolled rollout、少額の実資金を継続的に流すsynthetic transactionが例である
- production testは実際にお金を動かすため、ledger、reconciliation、audit trailを同じように通過させ、通常のcorrection/reversal経路で整理しなければならない
ドメイン用語と参考資料
- フィンテック入門では、コードよりもvocabularyとconceptのほうが難しいことがあるため、主要な用語を別途整理する
- 会計とledgerの領域には、ledger、general ledgerとsub-ledger、debit/credit、posting、chart of accounts、receivable/payable、IOU、accrual vs cash basis、trial balance、suspense/clearing account、write-off、commingling、reconciliation breakが含まれる
- MoneyとFXの領域には、Money type、minor units、basis point、notional、fiat vs crypto、stablecoin、pegged/wrapped/bridged、bid/ask/spread、mid-market rate、reference rate、mark-to-marketが含まれる
- 取引とsettlementの領域には、value date、booking date、settlement date、T+X、clearing vs settlement、cut-off time、float、netting、backdating、reversal/correctionが含まれる
- 決済、カード、市場、crypto、complianceの用語も別途整理されている
- PSP、omnibus account、FBO account、chargeback、issuer/acquirer、authorization vs capture
- order book、market vs limit order、maker/taker、slippage、liquidity、derivative、futures、perpetual、liquidation
- custody、hot/cold wallet、private key、multisig、MPC、gas、confirmation/finality、reorg、UTXO vs account model
- KYC、AML/CFT、sanctions screening、PEP、SoF/SoW、Travel Rule、VASP、MiCA、least privilege、RBAC、audit trail
- 参考資料は、会計とledger、paymentsとcards、marketsとtrading、crypto、engineering、KYCとAMLに分かれる
- Accounting for Computer Scientists: 複式簿記をグラフとデータモデルで説明するエンジニア向けの記事
- Modern Treasury, How to Scale a Ledger: production ledgerをソフトウェアエンジニアリングの観点から扱う記事シリーズ
- Designing Data-Intensive Applications: idempotency、log、consistency、failure modeをシステムの観点から扱う
3つのエンドツーエンドの例
-
Crypto withdrawal
- ユーザーが0.5 ETHを外部アドレスへ出金するフロー
- リクエストにはidempotency keyを含め、重複送信でも1件のwithdrawalしか作成されないようにする
- 0.5 ETHと見込みのnetwork feeをavailable balanceから予約する
- compliance gateはsanctions、AML、destination addressを確認し、外部呼び出しと手動レビューのため数日間sleepすることがある
- on-chain broadcastはidempotentである必要があり、クラッシュ後は2回目のbroadcastではなくchainを再確認しなければならない
- 十分なconfirmation後、ledgerにuser account debit、external on-chain account credit、network fee expense、service fee revenueをpostingする
- nightly jobがledgerとchain realityをreconcileする
-
Card deposit
- PSP経由のカード入金フロー
- ユーザーは金額とカード情報を送信し、PSPにidempotency keyでdeposit transactionを作成する
- authorizationはholdを作るだけで、まだ資金は会社のものではないためuser balanceをcreditしない
capturedwebhookはraw bytes signatureを検証し、raw payloadを保存し、すばやく2xxで応答した後に非同期で処理する- webhookは単なるtriggerなので、PSP APIでauthoritative stateを照会する
- captured but not settled状態はclearing accountを通じてpostingし、settlementはT+X後にバッチで到着することがある
- chargebackは元の記録を修正せず、linked compensating entryで処理する
-
In-app conversion with cashback
- 1,000 EURをUSDCに変換し、プロモーションcashbackを付与するフロー
- EUR→USDC quoteはUSDC→EURの逆数ではなく、directional rateである
- EURとUSDCは相互に加算せず、USDCは
(network, contract address)で識別され、pegged fiatと同一ではない - 計算では全体の精度を維持し、境界で1回だけ明示的な戦略に従って丸める
- spreadはrevenue accountに明示的にbookingしなければならず、rounding residualとして消えてはならない
- cashbackは無料のbalance bumpではなく、company promotional/expense accountからuser balanceへ移動する実際の資金である
- 結果のpublishはoutbox、CDC、event logのようなメカニズムでreliable deliveryを保証し、downstream consumerはstable event idでdedupeする
1件のコメント
Hacker News のコメント
ざっと見たところ、このハンドブックは浅く、一部の領域では悪いアドバイスに近いと思う
たとえば金額が整数でない形で保存されているのを見たら、悲鳴を上げて逃げ出したくなる。Rust の decimal が JSON の浮動小数点数として表現されるようなケースがあるからだ。よほど強い理由がない限り常に整数であるべきで、外部に出すビューは奇妙なビットコーディング形式でも何でもよい
外国為替レートも、特定の一時点だけで解決する問題ではない。買い手基準時点のレート、売り手基準時点のレート、合意、合意の許容誤差、合意済みの確定タイムスタンプといったものがすべて影響する
不変性のため、お金を扱うあらゆる場所にはイベントソーシングを置きたくなる。最終的に整理されたストリームは
A -> B -> Eのように見えるが、実際のストリームはA0 -> Edit(A0, A) -> B -> C -> D -> Rollback(B) -> Eかもしれない結局、Fintech もどれも同じ Fintech ではない。ある場所ではお金が荷物のように扱われ、別の場所ではお金がすべての中心だった
外国為替についても、ハンドブックの「正準レートは存在しない」という話を補強しているように見える。さらに記事は確定後の記録を扱っていて、こちらは確定方法を言っているのではないかと思う。別の目的に対する妥当なニュアンスではあるが、抜けている、あるいは間違っている証拠には見えない
不変性の部分も、記事が同じ要旨を述べているように思える。何が違うのか分からない
金利パス上で Monte Carlo によるオプション価格を計算し、デュレーション、コンベクシティ、ベガのようなリスク指標に関心があるなら、丸め規則が何かなど誰も気にしない。double で十分だ。
exp(-rt)cashflowや正規累積分布関数をどうやって整数に強制するつもりなのか?整数が正しい領域もある。だが普遍的な原則ではなく、正しい工学的選択をすればよい
使っている環境がサポートしているなら固定小数点も使えるが、技術的にはそれも依然として整数だ
表示用であれば decimal 値を返しても安全だ
金額を整数で保存するシステムから逃げるというのはいいね。そうすれば、おそらく同じシステムで働くことはなさそうだ。最近はむしろ、金額を整数として扱うシステムから逃げたくなることが多い。経験豊富な金融プログラマーだけが触る理想的なコードベースならうまくいくかもしれないが、そのようなシステムはたいてい、過度に排他的になったり脆弱になったりするリスクがある
金額表現に minor-unit precision 戦略を検討している人への助言としては、やめておいたほうがいい。少なくとも交換/API のデータ形式としては使うべきではない。
高速な整数演算、加算と減算で丸め問題がない、といった点は賢そうに見えるが、特定の通貨について暗黙に仮定した桁数が、別のパートナーと仕事をする瞬間に大きな痛手になり得る。特にステーブルコインは、それが代表する法定通貨と暗黙の小数桁数が異なる場合が多いので、なおさら重要だ。
JSON ベースの API では、金額を文字列型で表現することも検討に値する。JSON は十進精度を指定しないため、自分とすべてのユーザー/ベンダーのパーサー/シリアライザーが内部で浮動小数点を経由して精度を失っていないか、常に確認する必要がある。すぐに厄介なことになり得るし、文字列は概念的にはあまりきれいに見えないが、この問題を完全に回避できる。これをアンチパターン [1] と呼ぶ人もいるだろうが、ユーザーや株主の肩の上でイデオロギー的な純粋性のためにこの戦いをしたいとは思わない。
[1] https://blog.json-everything.net/posts/numbers-are-numbers-n...
高頻度取引の領域では、ある {slice} についてあらかじめ一貫した指数を確定できるなら、転送サイズを節約できる。たとえば商品/ティックサイズ/資産クラス/取引所/フィード/サーバーといった範囲で仮数だけを送り、クライアントはハードコードされた指数を使う、という形だ。
ただし似たような領域でも、転送データに
uint32の指数を 1 つ余分に送る価値があることは多い。そうすれば後で変更できるし、「今はセントだけ必要だ」といった初期設計に足を引っ張られずに済む。たとえば突然 bitcoin 価格を完全な精度でサポートしなければならないかもしれない。固定指数を調整したいときに破壊的変更の調整をしなくて済めば、ユーザーは感謝するはずだ。「任意」という基準は不合理だ。実際に証明できる価値もないまま、無制限のエンジニアリング予算を要求し得る、達成不能な標準だ。
標準の不在を特定し、実際のパーサーがどう動作しているかを語り、空白や満たされていないユースケースを議論するのはよい。より合理的な標準が必要だと提案するのもよい。だが、誰も実際には必要としておらず、意味も不明確で、実際には達成もできない「すべての可能性」を全員がサポートしろと求めるのは良い考えではない。
float/double、minor unit の 1000 分の 1 やそれより小さい単位の 固定小数点演算、任意精度の十進数、それともまったく別の方式なのか?プログラマーとして、Fintech のプログラマーたちがそれぞれ違う経験と観点から語るのを見ると、プログラミングがうまいとは一体何なのか気になってくる。
xlii が金額を浮動小数点で保存するなと言ったのは、よくある IEEE 754 の問題だ。金融の追跡は不変ログやイベントベースの記録で行うのが正しいが、周辺サービスすべてをイベントソーシングにする必要はないと思う。台帳、決済、注文、約定のようなコアロジックだけに適用しても十分だと思う。xlii の文章を見ると、モデリングが成功したときにだけ実現可能な手法のように見える。
lxgr の話は minor-unit の問題を指摘している。JSON 数値を言語やパーサーが浮動小数点としてパースすると、精度が失われる可能性がある。通常は値と別に小数桁数のフィールドを一緒に送る。ただし高頻度取引では、そのオーバーヘッド自体が高すぎるのでそうしないと聞いた。
antonymoose の話は、多くの本で語られている内容とつながっている。だから外国為替や API の文脈ではこうした設計が一般的だ。プロトコル設計のようにも感じる。
総合すると、誰もが自分のドメイン内では正しい。xlii のような人がシニアプログラマーならよいと思う一方で、自分がそのような複雑なシステムを設計できる気はしない。そういう意味で、それぞれの発言は妥当であり、ドメインによって意見が分かれる様子が興味深い。これが 専門性 なのかもしれない。
こういうものを見ると、プログラマーがどんな経験から来たのかをおおよそ推測できる。ときどきプログラミングは正解を探すことではなく、世界観を選ぶことのように感じる。
HN でプログラマーたちが自分のドメインをどうモデル化しているかを見るのは、いつも興味深い。たまにプロフィールを開いてみて、いつか使うかもしれないと思いながら、彼らのドメイン知識を個人 Wiki に追加している。
よい。この本には、すでに他の場所でも見つけられる良い情報が多く含まれているが、まとめられているだけでもかなり実用的だ。Kleppmann の Designing Data-Intensive Applications を強く勧める。第 1 版も非常によかったし、最近第 2 版が出た。
FinTech で CTO として働き、ソフトウェアスタック全体をゼロから作ったことがあるが、この本の教訓はおおむね正しい。おおむねと言ったのは、いつものように特定のプロジェクトでは「状況による」を多く考慮しなければならないからだ。たとえば私は、全体状態の計算問題を避けるためにイベントソーシングは使わなかった。標準的な append-only の監査証跡だけでも十分な場合がある。
exactly-once delivery は保証できないが、事実上の一回処理は構成できるし、実際に望むのもそちらだ。
すべてのリクエストとレスポンスを保存せよという話は完全に正しい。API を利用するときだけでなく、外部世界から何らかの情報を収集するときもそうすべきであり、可能なら境界内のすべての中間変換ステップも記録すべきだ。コンテンツアドレス指定のバケットとリレーショナルテーブルの組み合わせがよい。
また本文は データリネージについて何も述べていない。ベンダーが昼間に何らかのデータを更新し、その事実を必ず知る必要がある場合はどうするのか。以前の値を使った計算を再実行しても同じ結果が出るようにしつつ、その変化を説明できなければならない。解くのが特別に難しい問題ではないが、考慮は必要だ。
「悪い助言」とは、かなり穏当な表現だと思う。正直、この「ハンドブック」は大部分が LLM によって書かれたように見える
たとえば不変性のセクションにこんな文がある。「PIIを金融データから分離すれば、保持すべき金融履歴を失わずに削除権を尊重できる」
金融機関では、明白なKYC/AML上の理由から、この2つは一緒に扱われる
関連する期間が満了する前に、顧客名や住所などを要求されたら即座に消し、金融データだけを残しておくと、犯罪追跡のために合法的な機関がデータを求めに来たとき、組織全体が非常にまずい一日を過ごすことになる
Fintechで働こうとする人は、どこの管轄かも分からない誰かが書いた任意の「ハンドブック」に頼るべきではない
Fintechで働く人は、雇用主の内部ハンドブック/ガイドラインなどにのみ従って仕事をするべきだ。そうした文書は、会社の弁護士とコンプライアンス担当者が共同で作成し、雇用主が事業を行う管轄地域の法律や報告要件を満たすように作られているはずだ
私には、最終的に削除すべきPIIと、会計等式/不変条件に含まれるため実質的に永久保存したいデータを分離することを勧めているように見える。そうすれば、関連する記録保持期間が過ぎた後に前者を削除できる
どこの管轄かも分からない誰かが書いた「ハンドブック」に頼るべきではない、というのはその通りだ。だが、そこに出ているアイデアや実践を盲目的に無視したり、自分の組織の外を見なかったりするのもよくない。理想的には、それを読んだうえで自分の知識や現地の規制と照らし合わせ、調整すべきだ
完璧で誤りのない組織だけがある世界なら、雇用主の内部指針だけに従うというアプローチは合理的に見える。だが、こうした会話なしに、どうやってその水準に到達できるというのか?
この内容の大半は、Fintechだけでなく ソフトウェアエンジニアリング全般 に当てはまると思う
たとえばリトライ、冪等性、イベント順序などを扱う部分は、お金が直接関係しなくても、ある程度の正確性が必要なあらゆるシステムに当てはまる。「いつでもリトライすればよい」という前提で作られたシステムをあまりにも多く見てきたが、そもそもリトライできるためにはきれいに失敗しなければならず、下位システムが自分の想定するレベルの冪等性を提供していなければならない。こうした点は、実際には検証されていないことが多い
むしろ アカウントごとのデータベース のような、もっと過激なアプローチを擁護する記事を読みたい。Fintech内で固有のトレードオフがある、そういうものだ
Fintechのエンジニアや創業者に一番伝えたい助言は、初日からリスクとコンプライアンスを真剣に扱え、ということだ
金融システムは信頼を基盤としている。リスクを証明可能な形で緩和できなければ信頼を失い、最終的には事業全体を失うことになる
浮動小数点を避けろという話は事実ではない。Fintechで20年働いてきたが、ほとんどは double を使っていた。Excelもdoubleを使うし、フロントエンドもdoubleを使うはずで、すべてのデータベースがdoubleをサポートしている。標準ライブラリはdoubleのパースを知っているし、JSONも理屈はともかく実際にはdoubleを使っている。多くのERPシステムもdoubleを使う
doubleで通貨を扱うときの要点は、合計15桁の精度を保持できることを念頭に置くことだ。数値が
123456789.01や123.456789のようにそれ以上の桁数を使わない限り、金融計算で完全な十進精度を持てる。各計算の後と各比較の前に、必ず結果を15桁の精度内に丸めればよい。Excelはそうしているdoubleの最大の利点は広くサポートされており、システム内で異なる精度を混在させられることだ。国際金融や高度な金融商品を扱うと、そういうことが起きる。ある会計では千分の一までの精度が必要で、別のものは0.25の倍数に丸める必要がある。結局、基本的な算術は使わず、特化した会計数学ライブラリを使うことになるだろうし、そのライブラリはバックエンドとして浮動小数点をまったく問題なく使える
Plaidの残高確認 は、まもなく提出するACH引き落としが成功することの保証ではない
残高が100万ドルあろうと関係ない。ACHが処理される前に、すべてのお金が (a) 電信送金で出ていくか、(b) 昨日のACH、つまり請求書、口座振替などや小切手で清算されるか、(c) デビットカード/ATMで使われる可能性がある
なぜ一部のFintechがこれを処理していないと私が知っているのかは、たぶん言わないほうがいいだろう
冪等性キー のセクションだけでも読む価値がある。ほとんどの開発者はその教訓を苦労して学ぶ
それらの多くは冪等性に関する知識が広く普及する前のものなので、グローバルに一意でありそうな複数のフィールドをつなぎ合わせて、冪等性キーを無理やり作ることが多い。問題は、それが決して完全には一意ではないことだ。ときどき舞台裏をのぞけることがあり、たとえば銀行が同じ日に同じ金額を同じ受取口座へ送金できないようにしている場合がそれに当たる
冪等性 がどのように動作すべきか、なぜ重要なのかを説明するのに多くの時間を費やしてきた。ほとんどのチームは必要性を理解しているが、最初から考えていたチームはほとんどなかった
「Fintech」は非常に広く、Fintechと呼ばれるものの大半は実際には 通信 だ。会社間、トレーダー間、システム間、元帳間の通信である。業界全体に通用する「正しい」プログラミング方法はない。結局、正しい方法とは、自分が通信している相手が理解できる方法だからだ
相手が通貨をセント単位で追跡しているなら、それより高い精度で追跡すると丸めの不一致が生じる。逆にこちらはセント単位なのに相手が0.1セント単位で扱っていても同じだ。この文書の他の助言もすべて、そのように見るべきだ