トランザクション境界を設計する
金融処理では『順番に処理する』だけでは足りません。どこまでを全部成功・全部失敗にし、再送と競合をどう一度の結果へ収束させるかを設計します。
境界、transaction、並行制御、脅威、テスト、障害対応をFailure Firstで学ぶ。
先に学ぶ内容 — Prerequisite: 基礎 / 銀行システム学ぶこと — LEARNING GOALS
このページで学ぶこと
- Atomicityと業務境界
- Idempotency keyとfingerprint
- lock・constraint・rollbackの役割分担
体験29 · TRANSACTION DESIGN(境界を決める)
振込の依頼保存と資金移動の記帳が、別々のタイミングで確定してもよいか確かめよう
まずは予想をひと言。本文を読んだ後、教材内の参照元で実際のRPCとtestを確認します。
- 01まず予想する — PREDICT
まずここだけ:思ったことをひと言書く
振込のrequest保存と、実際の資金移動(journal記帳)が、別々のタイミングでcommitされてもよいと思いますか?
正解でなくて大丈夫です。「〜だと思う」から始めてみましょう。
ヒント1を見る
片方だけ確定した状態を、利用者や監査は説明できるでしょうか?
ヒント2を見る
『同じ意味の事実』はどこまで一緒に確定させる必要があるでしょうか?
もっと詳しく:この問いで使う専門用語
- Atomicityは、まとまり全体が成功するか、全部元に戻る性質です。
- Request fingerprintは、依頼内容を正規化して照合する指紋です。
- Failure injectionは、処理途中で意図的に失敗させるtestです。
日常語で理解する — PLAIN LANGUAGE
まず、ここだけつかむ
transaction境界は、途中で片方だけ残ると業務上壊れる変更をまとめます。
冪等性はkeyの存在ではなく、同一内容の再送・異内容の衝突・並行送信を一つの結果へ収束させる契約です。
成功testだけでなく、処理途中へ失敗を注入してrollbackを証明します。
順番に理解する — DIFFICULT PARTS, STEP BY STEP
難しいポイントを、順番にほどく
専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。
Atomicity
まとまり全体が成功するか、全部元に戻る性質です。
片側残高だけ減るなどの半端な状態を防ぎます。
振込のjournal、transfer、cache、auditをDB transactionで結びます。
Request fingerprint
依頼内容を正規化して照合する指紋です。
同じkeyを別金額へ再利用する誤用を検出します。
送金元・先・金額などから保存済み内容と比較します。
Failure injection
処理途中で意図的に失敗させるtestです。
通常成功するだけではrollback境界の欠落が見えません。
journal後・業務record後などで失敗させ最終recordを確認する考え方です。
発展 — ADVANCEDもっと詳しく確認する最初の学習では読まなくても問題ありません。仕組みをもっと深く理解したい場合に開いてください。
金融更新を戻す範囲と、失敗後に残す記録
金融更新の仕訳・transfers・残高cache・成功audit(TRANSFER_COMPLETEDと内側の変更trigger)は、confirm_transfer_otpの内側の例外blockでまとめてrollbackします。先に保存済みのrequestは消えません。transfer_auth.consumeによる認可済みintentの消費(consumed_at)、request=failed、TRANSFER_FAILEDは内側のrollbackの対象外で、外側のDB transactionがcommitすると残ります。
TOTPコードの正誤はDBではなくSupabase Authが判定します。DBはNext.jsのtransfer_mfa_broker経由で検証前に試行を予約し、その時点でintentのattemptsを加算します(累積5回まで、間隔30秒以上)。認証に失敗しても変わるのはintentとAuth側の状態だけで、requestはpending_otpのまま、金融データも変わりません。新しい認可がないままconfirmすると、transfer_auth.consumeが42501で拒否し、その呼出し全体がrollbackします。request=failedは金融更新の例外を捕捉した経路です。未捕捉例外・deferred constraint・commit自体の失敗なら、その呼出し全体の変更がrollbackするため、失敗記録の永続化を無条件には保証しません。
| 場面 | 金融更新 | 外側commit後の状態・event |
|---|---|---|
| 依頼作成 | まだ記帳しない | pending_otp、TRANSFER_OTP_REQUESTED(event名は従来のまま)。確認コードは生成しない |
| TOTPの誤入力・Auth検証失敗 | 資金移動に入らない | 予約時点でintent.attemptsが加算済み(累積5回まで)。requestはpending_otp |
| 新しい認可なしのconfirm(未認可・60秒超過・別session・内容変更) | consumeが42501で拒否し、その呼出し全体をrollback | requestはpending_otp、intentは未消費のまま |
| 認可後の資金移動で捕捉した例外 | 内側の仕訳・transfer・cache・成功auditをrollback | intent消費済み、request=failed、TRANSFER_FAILED |
| 成功 | 金融更新と成功auditを保存 | request=completed、transfer_id、intent消費済み |
| 外側transaction/commit失敗 | その呼出し全体をrollback | その呼出しの失敗eventも保証しない。過去のcommit済み依頼とは別 |
1つのDB transactionへ入れる判断基準
| 問い | 同じtransactionが必要な例 | 別処理にできる例 |
|---|---|---|
| 片方だけ成功してよいか | journalとjournal lines、振込と両口座残高 | 通知emailの送信 |
| 同時点の事実か | KYC状態変更とそのaudit | 日次集計reportの生成 |
| 外部systemを待つか | DB内の会計記帳 | SMS・外部決済network |
| 再試行方法を分離できるか | 資金移動の一意な確定 | 失敗した通知の再送queue |
安全なwrite pathの順番
- 01callerと対象ownershipを確認
- 02idempotency identityと内容を照合
- 03更新対象を決定順でlock
- 04lock後の状態・残高・上限を再確認
- 05業務recordとbalanced journalを作成
- 06派生cacheとauditを更新
- 07constraintを通してcommit
- 08応答を安全な公開形式へ変換
冪等性はkeyだけでは完成しない
| 状況 | 期待する結果 | 必要な仕組み |
|---|---|---|
| 同じkey・同じ内容 | 既存結果を返し、再記帳しない | unique identityと保存済みresult |
| 同じkey・違う金額 | 衝突として拒否 | canonical fingerprint |
| 同じ依頼を同時送信 | 1件だけ確定 | unique constraintと競合処理 |
| commit後に応答消失 | retryで最初の結果を取得 | resultをtransaction内に保存 |
| 最初の処理が失敗 | 再試行可能性をerror種別で判断 | 永続状態と一時障害の区別 |
失敗注入で境界を証明する
- journal作成後に例外を起こし、業務recordも残らないこと
- 片方の口座更新後に失敗し、両方戻ること
- 同じkeyを並行送信し、仕訳が1件だけであること
- deadlockまたはserialization failure時のretry方針
- timeout後の再送で最初のresultへ収束すること
実際のMinibankでは
振込RPCは認可、idempotency、口座lock、残高検査、journal、transfer、cached balance、auditをDB処理内で結びます。service層の順番だけでなく、migrationに定義された関数とconstraintを確認することが重要です。
外部通知のようなDB外の副作用は同じtransactionへ直接入れられません。本番設計ではoutboxなど、commit済みeventを安全に外へ配送する境界が追加で必要です。
体験学習の続き · LEARNING LOOP
ここまで読めましたか?続けて確認してみましょう
Atomicity・冪等性の考え方を読んだところで、教材内の参照元で実際のRPCと回帰testを確認し、自分の言葉で説明してみます。
- 02仕組みを確かめる — CONCEPT CHECK
画面操作なしで、仕組みを確かめる
この仕組みは、Mini Bankの画面には現れません。次のEvidenceとCode Trail、教材本文だけで理解を進めます。
- 03証拠を確認する — EVIDENCE
何を見たか、1つずつ確かめる
Evidence(判断の根拠となる記録)を確認したらチェックします。各項目の「何を見るか」を手がかりにしてください。
画面を開けない項目は、Source Trail(教材の参照元)と教材の説明で確認できます。失敗を実際に起こす必要はありません。
- 04コードの処理経路を追う — CODE TRAIL
コードを書く必要はありません
ここでは「説明されている仕組みが、実際のMini Bankのどこに実装されているか」を確認します。まずは日本語の役割説明を読み、入力した依頼がどこへ渡るかをつかみましょう。コードの全文を読む必要はありません。
まず振込を確定させるRPCを見て、次にその境界を検証する回帰testを見ます。どこまでが1つのtransactionかを確かめましょう。
教材の参照元を確認する — Source Trail ↓振込のwrite path。execute_transfer()がどこまでを一緒に確定させるかを探します。
supabase/migrations/20260823170001_create_transfer_beneficiary_and_otp.sql再送・競合・rollbackを検証する回帰test。
scripts/regression/suites/ - 05自分の言葉で説明する — EXPLAIN
LEVEL 1 — やさしい説明(まずここだけ)
なぜ振込のjournal・transfer・cache・auditを1つのDB transactionへまとめる必要があるのでしょうか? 短い2文を目安に説明してください。
日常語だけで完了できます。専門用語の数や正誤を自動採点するものではありません。
ヒント1を見る
「Atomicity」と「fingerprint」という2つの言葉を使って書いてみましょう。
ヒント2を見る
通知メールと資金移動を同じtransactionにできない理由を考えてみましょう。
もっと詳しく:LEVEL 2 — 専門用語を使った説明(任意)
Atomicity、Request fingerprint、Failure injectionの3つを使って、transaction境界の設計方法を説明してみましょう。
挑戦する場合は、上の説明欄に追記できます。追加しなくても完了できます。
説明の確認ポイント — Checkpoint- 1つのtransactionへまとめる判断基準を説明できる
- 成功testだけではrollback境界を証明できないと説明できる
理解を確かめる — CHECK YOUR UNDERSTANDING
3問で理解チェック
答えを頭の中で考えてから、各項目を開いて確認してください。
Q1通知emailも資金移動と同じDB transactionにできますか?
通常できません。DB外副作用なのでoutboxなど別の信頼できる配送境界が必要です。
Q2同じkeyで別金額が来たら既存結果を返しますか?
返しません。fingerprint不一致として衝突を拒否します。
Q3rollbackをどう証明しますか?
途中地点へ失敗を注入し、関連する業務record・仕訳・残高・auditが半端に残らないことを確認します。
完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。
教材の参照元 — SOURCE TRAIL
根拠と関連教材
CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。
確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。
- 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
supabase/migrations/20260920074653_transfer_totp_intent_authorization.sqlpublic.request_transfer / public.confirm_transfer_otp / public.reissue_transfer_otp / public.prepare_transfer_mfa#128。依頼は確認コードを生成せずotp_plaintext=null。confirmはtransfer_auth.consumeで認可済みintentを消費してからexecute_transferを呼ぶ。reissueは常に42501。稼働DBへの適用はUNKNOWN。 - 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
supabase/migrations/20260920083426_require_fresh_totp_on_intent_session.sqlpublic.transfer_mfa_broker#129。intentのAuth session自身に、予約・検証より後のTOTP AMRを要求する最新broker定義。 - 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
supabase/migrations/20260920115556_scalar_journal_posting_atomic_cache.sqlpublic.execute_transfer最新定義。ledger.post_journal_entryの戻り値をscalar uuidで受ける。検証・lock・上限・冪等性は従来どおり。既存ACLは維持。 - 現在の実装 — CURRENT IMPLEMENTATION
src/app/api/v1/transfers/mfa/route.tsintent準備・challenge束縛・試行予約・認可完了のserver入口。TOTPコードは受け取らない。 - 現在の実装 — CURRENT IMPLEMENTATION
src/components/auth/TransferMfa.tsxブラウザがSupabase Authへchallenge/verifyを直接送る部分。成功の自己申告は認可にならない。 - 現在の実装 — CURRENT IMPLEMENTATION
src/app/api/v1/transfers/route.ts認証済みの旧直接振込入口はOTP_REQUIREDを返す。 - 現在の実装 — CURRENT IMPLEMENTATION
src/lib/errors/apiError.tsOTP_REQUIREDのHTTP statusは409。 - 現在の実装 — CURRENT IMPLEMENTATION
scripts/mfa/intent-db.test.mjsintent束縛・回数/期限・単回消費・session検査のSQL受入test。Auth行はfixtureで、実Authの証明ではない。当日のPASSも意味しない。 - 過去の実装 — HISTORICAL IMPLEMENTATION
src/lib/config/demoOtpDisplay.ts#128以前のデモコード表示フラグ。v7ではDBがコードを返さず、Productionでは常にfalse。 - 過去の実装 — HISTORICAL IMPLEMENTATION
scripts/regression/suites/transferOtp.mjs#128以前のデモOTP(誤入力・再発行・期限)の回帰test。v7の認可方式の根拠にしない。 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170001_create_transfer_beneficiary_and_otp.sqlexecute_transfer()振込write path - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260819155222_create_journal_balance_constraint_trigger.sqlcommit時の借貸一致 - 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/suites/再送・競合・rollbackの検証