Transaction:Javaで境界を増やさない
既存教材のTransactionを残したまま、Java / Spring Bootから見たトランザクション境界を学びます。重要なのは@Transactionalを付けることではなく、どこが原子性を保証するかです。
Java / Spring Bootを入口に、金融システムのService・Repository・Transaction設計を読む。
先に学ぶ内容 — Prerequisite: ソフトウェア工学 / 銀行システム学ぶこと — LEARNING GOALS
このページで学ぶこと
- @Transactionalが効く範囲
- HTTP越しRPCとDB transactionの違い
- Payment Serviceに@Transactionalがない理由
体験15 · JAVA TRANSACTION(境界を増やさない)
Javaの注釈が、どこまで「取り消し」を保証するか確かめよう
まずは予想をひと言。本文を読んだ後、教材内の参照元で実際のJavaコードを確認します。
- 01まず予想する — PREDICT
まずここだけ:思ったことをひと言書く
Javaのコードに「トランザクション」と書けば、複数のシステムをまたぐ処理も1つの取り消し可能な単位になるでしょうか?
正解でなくて大丈夫です。「〜だと思う」から始めてみましょう。
ヒント1を見る
電話で別の店舗に会計処理を頼んだ後、自分の机で作業を取り消したら、電話先の会計も戻るでしょうか?
ヒント2を見る
Javaのメソッドの開始と終了は、データベースの処理の開始と終了と必ず同じでしょうか?
もっと詳しく:この問いで使う専門用語
- @Transactionalは、主に同じDB接続上の処理をまとめるSpringの仕組みです。
- 原子性は、全部成功するか全部なかったことになる性質です。
- RPC(Remote Procedure Call)は、別プロセスの処理をHTTP経由で呼び出す仕組みです。
- 補償処理は、確定済みの処理を打ち消すために追加で行う業務操作です。
日常語で理解する — PLAIN LANGUAGE
まず、ここだけつかむ
トランザクションの目的は注釈を付けることではなく、途中状態を残さない正式な境界を決めることです。
Payment ServiceはDBへ直接接続せずHTTPでRPCを呼ぶため、Javaの@TransactionalはRPC先の変更を巻き戻せません。
現在は1回のRPC内部でPostgreSQLが原子性を保証するため、PaymentApplicationServiceへ意図的に@Transactionalを付けていません。
振込の途中でお金の記録の更新に失敗すると、その部分だけ元の状態に戻します。ただし、振込を依頼した記録自体は消えず、「失敗した」という記録として残ります。この考え方を「ロールバック」と呼びます。
順番に理解する — DIFFICULT PARTS, STEP BY STEP
難しいポイントを、順番にほどく
専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。
@Transactional
主に同じDB connection上の処理をまとめるSpringの仕組みです。
複数DB更新をcommitかrollbackへまとめるためです。
Payment ServiceはHTTP RPCなので現在は使いません。
原子性
全部成功するか、全部なかったことになる性質です。
振込元だけ減るような半端な状態を防ぐためです。
金融更新の仕訳・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すると残ります。
補償処理
確定済み処理を打ち消す別の業務操作です。
複数システムの確定を後から単純rollbackできないためです。
将来の分散処理では逆仕訳など明示的な操作が必要です。
発展 — 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済み依頼とは別 |
Java経路の実装と稼働確認を分ける
request/confirmはPAYMENT_BACKENDで選択します。未設定・空文字・legacyはNext.js→PostgREST RPC、javaはNext.js→Java Payment Service→PostgREST RPCです。不正な値はエラー、Javaのtimeout・5xx・通信断でもlegacyへ自動fallbackしません。旧OTP再発行はこの切替の対象外で、v7のRPCは常に42501で拒否します。TOTPの認可準備はNext.jsの/api/v1/transfers/mfaが担います。コードの存在は確認済みですが、本番・PreviewでJavaが選択されているかはUNKNOWNです。
- 01Next.jsがPAYMENT_BACKENDを読む
- 02legacy:呼出者tokenでPostgRESTへ / java:Java Payment ServiceへHTTP
- 03request_transfer / confirm_transfer_otpでDB処理
- 04Javaの@TransactionalではHTTP先のcommitを取り消せない
- 05応答消失時は結果不明。別backendへ自動fallbackしない
トランザクションは『全部成功か全部失敗』の境界
金融更新の仕訳・transfer・残高cache・成功auditは原子的に扱います。ただし依頼全体を消すわけではありません。confirm_transfer_otpは金融更新の例外を内側blockで捕捉し、外側に認可済みintentの消費・request=failed・TRANSFER_FAILEDを残してoutcomeを返します。外側がcommitして初めて保存され、未捕捉例外やcommit失敗ではその呼出し全体がrollbackします。
Javaから見えるのはHTTP RPCの成功または失敗です。Javaメソッドの開始と終了が、そのままDB transactionの開始と終了になるわけではありません。
@Transactionalは魔法の分散トランザクションではない
| 処理 | @Transactionalでまとめられるか | 理由 |
|---|---|---|
| 同じDataSourceへのJDBC更新 | 通常は可能 | Springが同じDB connectionのcommitを管理 |
| PostgRESTへのHTTP呼び出し | まとめられない | 別プロセス・別connectionで完結 |
| 外部通知やメール | まとめられない | DB以外の副作用 |
| PostgreSQL RPC内部の複数更新 | RPC側で原子的 | 関数を実行するDB transactionが管理 |
PaymentApplicationServiceに@Transactionalがない理由
createPayment()とconfirmOtp()は、それぞれ1回のRPC呼び出しへ委譲します。正式なtransaction境界はPostgreSQL側です。
Java側に形だけのtransactionを追加すると、原子性が広がったように誤解しやすくなります。現在の設計では、Javaをステートレスなオーケストレーターとして保つ方が境界を正確に表します。
将来、複数の外部処理をつなぐなら
- どの記録がSystem of Recordか決める
- DB commit後の通知にはoutboxなどを検討する
- 再試行可能な操作へidempotencyを設ける
- 補償処理は削除ではなく明示的な逆操作にする
- 途中状態を保存し、監視と人手復旧を設計する
体験学習の続き · LEARNING LOOP
ここまで読めましたか?続けて確認してみましょう
トランザクション境界の考え方を読んだところで、教材内の参照元で実際のJavaコードを確認し、自分の言葉で説明してみます。
- 02仕組みを確かめる — CONCEPT CHECK
画面操作なしで、仕組みを確かめる
この仕組みは、Mini Bankの画面には現れません。次のEvidenceとCode Trail、教材本文だけで理解を進めます。
- 03証拠を確認する — EVIDENCE
何を見たか、1つずつ確かめる
Evidence(判断の根拠となる記録)を確認したらチェックします。各項目の「何を見るか」を手がかりにしてください。
画面を開けない項目は、Source Trail(教材の参照元)と教材の説明で確認できます。失敗を実際に起こす必要はありません。
- 04コードの処理経路を追う — CODE TRAIL
コードを書く必要はありません
ここでは「説明されている仕組みが、実際のMini Bankのどこに実装されているか」を確認します。まずは日本語の役割説明を読み、入力した依頼がどこへ渡るかをつかみましょう。コードの全文を読む必要はありません。
まずJavaのユースケース層を見て、次にPostgreSQLへの接続方法を見ます。原子性の最終的な保証がどちらの層にあるかを確かめましょう。
教材の参照元を確認する — Source Trail ↓振込のユースケースを担うService。@Transactionalが無く、1回のRPC呼び出しへ委譲する構造を探します。
services/payment-service/src/main/java/com/minibank/payment/application/PaymentApplicationService.javaPostgreSQLへ直接接続せず、HTTP経由でRPCを呼ぶ境界。原子性の保証がどこにあるかを探します。
services/payment-service/src/main/java/com/minibank/payment/repository/SupabaseRpcClient.java - 05自分の言葉で説明する — EXPLAIN
LEVEL 1 — やさしい説明(まずここだけ)
なぜPayment Serviceの主要な処理には@Transactionalが付けられていないのでしょうか? 短い2文を目安に説明してください。
日常語だけで完了できます。専門用語の数や正誤を自動採点するものではありません。
ヒント1を見る
「@Transactional」と「HTTP越しのRPC」という2つの言葉を使って書いてみましょう。
ヒント2を見る
Javaが失敗を検知したときと、DBの処理が実際に確定したときの前後関係を考えてみましょう。
もっと詳しく:LEVEL 2 — 専門用語を使った説明(任意)
@Transactional、原子性、補償処理の3つを使って、複数システムをまたぐ処理をどう安全に保つか説明してみましょう。
挑戦する場合は、上の説明欄に追記できます。追加しなくても完了できます。
説明の確認ポイント — Checkpoint- Javaの注釈がHTTP先の処理を巻き戻せないと説明できる
- 原子性の最終的な保証がPostgreSQL側にあると説明できる
理解を確かめる — CHECK YOUR UNDERSTANDING
4問で理解チェック
答えを頭の中で考えてから、各項目を開いて確認してください。
Q1Javaのコードがあれば本番で稼働中ですか?
request/confirmはPAYMENT_BACKENDで選択します。未設定・空文字・legacyはNext.js→PostgREST RPC、javaはNext.js→Java Payment Service→PostgREST RPCです。不正な値はエラー、Javaのtimeout・5xx・通信断でもlegacyへ自動fallbackしません。旧OTP再発行はこの切替の対象外で、v7のRPCは常に42501で拒否します。TOTPの認可準備はNext.jsの/api/v1/transfers/mfaが担います。コードの存在は確認済みですが、本番・PreviewでJavaが選択されているかはUNKNOWNです。
Q2Serviceへ@Transactionalを付ければHTTP先もrollbackしますか?
しません。PostgREST側は別connection・別transactionです。
Q3失敗時に金融更新と依頼状態は同じように戻りますか?
金融更新の仕訳・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すると残ります。
Q4複数の外部処理を追加する時に必要なものは?
状態保存、冪等性、再試行、補償、監視などを含む分散処理設計が必要です。
完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。
教材の参照元 — SOURCE TRAIL
根拠と関連教材
CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。
確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。
Implementation Status: IMPLEMENTED IN MINIBANK(この教材が扱うコードの範囲)
Runtime Verification Status: UNKNOWN(本番・Previewでの経路選択)
- 現在の実装 — 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の認可方式の根拠にしない。 - 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
src/lib/config/paymentBackend.tsgetPaymentBackend切替の許容値・default・不正値拒否。稼働環境の値はUNKNOWN。 - 現在の実装 — CURRENT IMPLEMENTATION — Primary Source
src/services/TransferService.tsrequestTransfer / confirmTransferOtp / reissueTransferOtprequest/confirmのみbackend切替。残存createTransfer関数のコメントは現ACLの根拠にしない。 - 現在の実装 — CURRENT IMPLEMENTATION
src/services/JavaPaymentBackend.tsJavaへのHTTP adapterとエラー処理。自動fallbackなし。 - 現在の実装 — CURRENT IMPLEMENTATION
services/payment-service/src/main/java/com/minibank/payment/application/PaymentApplicationService.java@Transactionalを付けない設計理由 - 現在の実装 — CURRENT IMPLEMENTATION
services/payment-service/src/main/java/com/minibank/payment/repository/SupabaseRpcClient.javaDBへ直接接続せずHTTP RPCを呼ぶ境界 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/実際の原子性を担うPostgreSQL関数