学習・技術検証用デモ — 本システムは実際の銀行サービスではありません。

公開情報をもとに構築した教育目的のシステムです。実在する個人情報・口座情報・機密情報・認証情報等を入力しないでください。

ミニ銀行ACADEMY SYSTEM LEARNING
教材メニュー
学習進捗0 / 134
あと134項目
ADVANCED · 発展COURSE 05 · JAVA / ENTERPRISE DEVELOPMENT

Idempotency:再送を二重送金にしない

通信タイムアウト後の再送は正常な利用行動です。Java APIがIdempotency-Keyを受け取り、既存PostgreSQLの重複排除へ安全に渡す流れを学びます。

08 / 11
JAVA / ENTERPRISE DEVELOPMENT · Java・エンタープライズ開発

Java / Spring Bootを入口に、金融システムのService・Repository・Transaction設計を読む。

先に学ぶ内容 — Prerequisite: ソフトウェア工学 / 銀行システム

学ぶこと — LEARNING GOALS

このページで学ぶこと

  • 再試行と二重実行の違い
  • Idempotency-KeyのAPI契約
  • 同じキーで異なる内容を拒否する理由

体験16 · JAVA IDEMPOTENCY(再送を二重にしない)

同じ操作を再送しても、二重に実行されないか確かめよう

まずは予想をひと言。本文を読んだ後、教材内の参照元で実際のJavaコードを確認します。

体験学習 · STEP 1 / 5
  1. 01
    まず予想する — PREDICT

    まずここだけ:思ったことをひと言書く

    同じ振込を誤ってもう一度送信してしまったら、必ず二重に振込が実行されてしまうでしょうか?

    正解でなくて大丈夫です。「〜だと思う」から始めてみましょう。

    ヒント1を見る

    応答だけが届かず、依頼自体はサーバーで成功していたら、利用者はどう行動すると思いますか?

    ヒント2を見る

    同じ受付番号で、金額だけ変えて送り直したら、前の結果をそのまま返してよいでしょうか?

    もっと詳しく:この問いで使う専門用語
    • Idempotency-Keyは、同じ操作の再送を識別する受付番号です。
    • fingerprintは、依頼内容を比較するための識別値です。
    • 永続的な重複排除は、再起動後もDBの記録と制約で重複を防ぐ仕組みです。
まずはひと言だけで大丈夫です。

この後の本文を最後まで読んでから、実際に操作して確かめるステップへ進みます。

日常語で理解する — PLAIN LANGUAGE

まず、ここだけつかむ

1

サーバーで処理が成功しても、応答だけ失われれば利用者や画面は同じ操作を再送します。これは異常な利用ではありません。

2

Java APIはIdempotency-Keyを必須UUIDとして受け取り、既存DBの永続的な重複排除へ渡します。

3

同じキーで同じ内容なら同じ結果へ収束させ、異なる内容ならconflictとして拒否します。

4

手動SQLやCLIも再試行され得ます。書込み経路ごとに冪等性を設計しなければ、append-onlyのLedgerへ重複が永久に残ります。

順番に理解する — DIFFICULT PARTS, STEP BY STEP

難しいポイントを、順番にほどく

専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。

01

Idempotency-Key

やさしく言うと

同じ操作の再送を識別する受付番号です。

なぜ必要?

タイムアウト後の再試行を二重送金にしないためです。

Minibankでは

UUIDをp_idempotency_keyへ渡します。

02

fingerprint

やさしく言うと

依頼内容を比較するための識別値です。

なぜ必要?

同じキーで金額や振込先を変える操作を拒否するためです。

Minibankでは

DB側が同一依頼かconflictかを判断します。

03

永続的な重複排除

やさしく言うと

再起動後もDB記録と制約で重複を防ぐ仕組みです。

なぜ必要?

Javaメモリだけでは複数台・再起動・競合に耐えないためです。

Minibankでは

DBのunique制約と既存結果を使います。

発展 — ADVANCEDもっと詳しく確認する最初の学習では読まなくても問題ありません。仕組みをもっと深く理解したい場合に開いてください。

成功したのに応答だけ失われる

再送を禁止すると障害回復が難しくなります。安全に再送できる契約を作ることが重要です。

  1. 01利用者が振込を送信
  2. 02サーバーとDBでは依頼作成に成功
  3. 03応答がネットワークで失われる
  4. 04画面が同じ操作を再送
  5. 05同じ依頼として認識できなければ2件目が作られる

Javaはキーを検証して、そのまま業務境界へ渡す

ControllerはIdempotency-Keyヘッダーを必須のUUIDとして検証します。ServiceとGatewayを通じてp_idempotency_keyへ渡します。

Javaプロセスのメモリで重複を覚えません。再起動、複数インスタンス、競合に耐えるため、永続的な一意性と判定はDB側に置きます。

UUID idempotencyKey = UUID.fromString(idempotencyKeyHeader);
paymentApplicationService.createPayment(
    authorizationHeader,
    command,
    idempotencyKey
);

同じキーなら何でも同じ結果、ではない

同じ利用者・同じキー・同じ依頼内容なら、既存結果を返せます。しかし同じキーで金額や振込先が違う場合は、古い結果を返してはいけません。

DB側は依頼内容のfingerprintを比較し、矛盾する再利用をTR009として拒否します。JavaはIDEMPOTENCY_KEY_CONFLICTへ変換します。

再送期待する扱い
同じキー・同じ内容同じ業務結果へ収束
同じキー・異なる内容409相当のconflictとして拒否
異なるキー・同じ内容別依頼になり得るためUI側でもキー再利用方針が必要

冪等性は全操作へ自動で付く性質ではない

  • キーのscopeを利用者・操作種別と組み合わせる
  • 保存期間と再利用可能期間を決める
  • 結果を返す前に永続化する
  • 競合時もunique制約で1件にする
  • 下流処理にも独自の重複排除が必要か確認する

実例:1つのSQL文が仕訳を8件作った(原因は再試行ではなかった)

v1.2.0のJournal Trace検証では、linked環境へCLIから SELECT (ledger.post_journal_entry(...)).* を直接実行し、1回の実行につき同一時刻の仕訳が8件作られました。2回の実行で、均衡した仕訳16件ができ、口座の残高cacheは更新されませんでした。以前は「CLIまたはManagement API層の再試行が原因の可能性が高い」と記録していましたが、2026-09-20の再調査で、この推定は保存されたコマンドと隔離環境での再現によって否定されました。

確定した原因はPostgreSQLの複合型の展開です。当時の関数は8属性のledger.journal_entries行を返すVOLATILE関数でした。SELECTリストの (f(...)).* は属性ごとの参照へ展開され、関数が属性の数だけ評価されます。外部の再試行や並行実行がなくても、1つの文で8回投稿されます。SELECT * FROM f(...) なら1回だけ評価されますが、それでも内部helperは業務request・idempotency記録を持たない不完全な書込み経路です。

v7のmigration 20260920115556はhelperの戻り値をscalar uuidへ変え、(...).* がside effectの前にエラーになるようにしました。helperは影響する口座をlockし、同じtransaction内で残高cacheを再計算します。20260920145950はさらに、既存のcache差異がある口座への通常postingをLG018で拒否します。どちらも、helperを冪等な銀行業務の入口にするものではありません。

体験学習の続き · LEARNING LOOP

ここまで読めましたか?続けて確認してみましょう

Idempotency-Keyの考え方を読んだところで、教材内の参照元で実際のJavaコードを確認し、自分の言葉で説明してみます。

STEP 2〜5 / 5
  1. 02
    仕組みを確かめる — CONCEPT CHECK

    画面操作なしで、仕組みを確かめる

    この仕組みは、Mini Bankの画面には現れません。次のEvidenceとCode Trail、教材本文だけで理解を進めます。

  2. 03
    証拠を確認する — EVIDENCE

    何を見たか、1つずつ確かめる

    Evidence(判断の根拠となる記録)を確認したらチェックします。各項目の「何を見るか」を手がかりにしてください。

    画面を開けない項目は、Source Trail(教材の参照元)と教材の説明で確認できます。失敗を実際に起こす必要はありません。

  3. 04
    コードの処理経路を追う — CODE TRAIL

    コードを書く必要はありません

    ここでは「説明されている仕組みが、実際のMini Bankのどこに実装されているか」を確認します。まずは日本語の役割説明を読み、入力した依頼がどこへ渡るかをつかみましょう。コードの全文を読む必要はありません。

    まずAPIの入口でキーがどう検証されるかを見て、次にDBからの応答がどうエラーへ変換されるかを見ます。

    HTTP入口。Idempotency-Keyヘッダーを必須のUUIDとして検証する箇所を探します。

    services/payment-service/src/main/java/com/minibank/payment/controller/PaymentController.java

    DBからのconflict応答を、Javaのエラーへ変換する箇所を探します。

    services/payment-service/src/main/java/com/minibank/payment/error/RpcErrorMapper.java
    教材の参照元を確認する — Source Trail ↓
  4. 05
    自分の言葉で説明する — EXPLAIN

    LEVEL 1 — やさしい説明(まずここだけ)

    なぜ同じIdempotency-Keyでも、内容が異なる場合には古い結果を返してはいけないのでしょうか? 短い2文を目安に説明してください。

    日常語だけで完了できます。専門用語の数や正誤を自動採点するものではありません。

    ヒント1を見る

    「同じ内容」と「異なる内容」という2つの言葉を使って書いてみましょう。

    ヒント2を見る

    Javaのメモリだけで再送を覚えると何が困るか考えてみましょう。

    もっと詳しく:LEVEL 2 — 専門用語を使った説明(任意)

    Idempotency-Key、fingerprint、永続的な重複排除の3つを使って、再送が安全に扱われる仕組みを説明してみましょう。

    挑戦する場合は、上の説明欄に追記できます。追加しなくても完了できます。

    説明の確認ポイント — Checkpoint
    • 再送そのものは異常な利用ではないと説明できる
    • 同じキーでも内容が違えば拒否されると説明できる
予想5文字以上・証拠2項目・説明20文字以上で完了できます。

証拠の確認:0 / 2。LEVEL 2(専門用語での説明)は任意です。

入力はこのブラウザに保存されます。Academy教材の進捗とは別の学習メモです。

理解を確かめる — CHECK YOUR UNDERSTANDING

4問で理解チェック

答えを頭の中で考えてから、各項目を開いて確認してください。

Q1同じキーで金額を変えたら古い結果を返しますか?
答え

いいえ。IDEMPOTENCY_KEY_CONFLICTとして拒否します。

Q2JavaのMapに処理済みキーを保存すれば十分ですか?
答え

不十分です。再起動や複数インスタンス、同時競合で失われます。

Q3異なるキーで同じ依頼を送れば必ず重複排除されますか?
答え

キーの契約上は別依頼になり得ます。呼出側にも操作単位でキーを再利用する設計が必要です。

Q4手動のSQLならIdempotency-Keyは不要ですか?
答え

不要とは限りません。CLIや通信層の再試行でも書込みは重複し得るため、再実行可能な操作は冪等な境界を通します。

ここまで読めましたか?

完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。

教材の参照元 — SOURCE TRAIL

根拠と関連教材

CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。

確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。

  • 現在の実装 — CURRENT IMPLEMENTATIONservices/payment-service/src/main/java/com/minibank/payment/controller/PaymentController.javaparseIdempotencyKeyIdempotency-Keyの必須・UUID検証
  • 現在の実装 — CURRENT IMPLEMENTATIONservices/payment-service/src/main/java/com/minibank/payment/repository/PaymentRpcGateway.javarequestTransferp_idempotency_keyへの引き渡し
  • 現在の実装 — CURRENT IMPLEMENTATIONservices/payment-service/src/main/java/com/minibank/payment/error/RpcErrorMapper.javaTR009をconflictへ変換
  • 過去の実装 — HISTORICAL IMPLEMENTATIONsupabase/migrations/永続的なunique制約とfingerprint判定
  • 現在の実装 — CURRENT IMPLEMENTATIONsupabase/migrations/20260920145950_reject_preexisting_balance_drift.sqlledger.post_journal_entry最新定義。scalar uuidを返し、口座lock後に既存のcache差異をLG018で拒否し、同じtransactionでcacheを再計算
  • 過去の実装 — HISTORICAL IMPLEMENTATIONsupabase/migrations/20260920115556_scalar_journal_posting_atomic_cache.sqlledger.post_journal_entry戻り値を複合型からscalar uuidへ変え、(...).* の多重評価を副作用前に失敗させた変更
  • 過去の実装 — HISTORICAL IMPLEMENTATIONscripts/release/reproduce-historical-composite-posting.mjs隔離PG17環境で (f(...)).* が1文8件、SELECT * FROM f(...) が1件になることを再現したscript
  • 未確認 — UNKNOWNdocs/delivery/V7-PRODUCTION-BLOCKER-FORENSICS-20260920.md調査記録。保存コマンド・journal ID・再現結果の照合。実行者本人の認証記録や当時のDB transaction IDはなく、未確定部分が残る
  • 未確認 — UNKNOWNREADME.md仕様資料。現行実装との一致は未確認。Operational Notesは再試行説を否定し、複合型展開を原因として訂正済み