トランザクション設計は業務処理の境界で考える
トランザクション設計で迷う場面は、Spring Boot実務ではかなり多いです。トランザクションアノテーションをどこに付けるか。rollbackされる例外は何か。Service層をどこで分けるか。こうした判断は、アノテーションを付けるだけでは決まりません。
結論から言うと、Spring Bootでは業務境界が重要です。「1つの業務処理として成功または失敗させたい範囲」を明確にします。
この記事では、Java/Spring Bootの実務でよく迷う点を整理します。トランザクションアノテーションの付け方、rollback、propagationを扱います。さらに、Service層の境界やレビューで見られやすいポイントも解説します。
Springの宣言的管理を理解する
まず、Spring Frameworkには一貫したトランザクション管理の仕組みがあります。公式のTransaction Managementでも説明されています。JTA、JDBC、Hibernate、JPAなどをまたいだモデルです。
実務では、トランザクションアノテーションを使うことが多いです。明示的にcommitやrollbackを書くより一般的です。Spring公式のDeclarative Transaction Managementでも説明されています。宣言的トランザクション管理という考え方です。
ただし、宣言的に扱えるからといって、設計を考えなくてよいわけではありません。むしろ、どのServiceメソッドをトランザクション境界にするかが重要になります。
トランザクションはService層の業務処理に付けるのが基本
次に、トランザクションアノテーションはService層に付けることが多いです。RepositoryではなくService層です。理由は、トランザクションの境界がDB操作単体ではなく、業務処理単位で決まるためです。
@Service
public class OrderService {
@Transactional
public void placeOrder(OrderRequest request) {
Order order = orderRepository.save(request.toOrder());
paymentRepository.save(Payment.pending(order));
stockService.reserve(request.items());
}
}
この例では、注文作成、支払い作成、在庫予約を1つの業務処理として扱います。途中で失敗した場合、全体をrollbackしたい範囲です。そのため、Serviceメソッドが自然な境界になります。
一方で、Repositoryメソッドごとに分けると問題が起きます。業務処理全体の一貫性を守りにくくなるためです。例えば、注文だけ保存され、支払い作成に失敗する状態が起きる可能性があります。
Spring Boot トランザクション設計ではrollback条件を理解する
特に誤解されやすいのがrollback条件です。Springのトランザクションアノテーションは、デフォルトでは非検査例外でrollbackします。RuntimeExceptionなどです。一方で、checked exceptionでは注意が必要です。自動rollbackされないケースがあります。
Spring公式のUsing @Transactionalでも、rollbackルールが説明されています。アノテーションの設定も確認できます。つまり、例外設計とDB更新の境界は関連します。
@Transactional(rollbackFor = ApplicationException.class)
public void approve(Long orderId) throws ApplicationException {
Order order = orderRepository.findById(orderId)
.orElseThrow(ApplicationException::new);
order.approve();
}
ただし、rollbackForを機械的に付ければよいわけではありません。どの例外でDB更新を取り消すべきかを業務ルールとして決める必要があります。
| 例外の種類 | 考え方 | 確認ポイント |
|---|---|---|
| RuntimeException | デフォルトでrollbackされやすい | 業務例外として使いすぎていないか |
| checked exception | 設定しないとrollbackされない場合がある | rollbackForが必要か |
| 外部API失敗 | DB更新と一緒に戻せないことがある | 補償処理が必要か |
特に、外部APIやメッセージ送信を含む処理では注意が必要です。DBはrollbackできます。一方で、外部サービスに送ったリクエストは取り消せない場合があります。
readOnlyは読み取り専用の意図を表す
検索処理では、readOnlyを使うことがあります。トランザクションを読み取り専用にする指定です。これは「この処理では更新しない」という意図を表します。
@Transactional(readOnly = true)
public OrderDetail getOrderDetail(Long orderId) {
return orderRepository.findDetail(orderId);
}
ただし、readOnlyを付ければ必ず高速になるとは限りません。DB、JPAプロバイダ、設定によって効果は変わります。それでも、読み取り専用の意図をコード上に残せる点は実務上のメリットです。
一方で、readOnlyのメソッド内でEntityを変更する実装は避けるべきです。レビューでは、読み取り用Serviceと更新用Serviceを確認します。責務が混ざっていないかを見るためです。
propagationは例外的な用途から考える
Springのトランザクション伝播では、REQUIRED、REQUIRES_NEW、NESTEDなどを指定できます。公式のTransaction Propagationでも説明されています。それぞれ意味が異なります。
通常は、デフォルトのREQUIREDで十分なことが多いです。呼び出し元にトランザクションがあれば参加し、なければ新しく開始します。
一方で、REQUIRES_NEWは独立したトランザクションを作ります。監査ログだけは本処理が失敗しても残したい、といった場面で検討されます。
| propagation | 使いどころ | 注意点 |
|---|---|---|
| REQUIRED | 通常の業務処理 | 外側のトランザクションに参加する |
| REQUIRES_NEW | 監査ログ、独立保存 | 別トランザクションになり接続数にも注意 |
| NESTED | 部分rollback | DBやTransactionManagerの対応が必要 |
ただし、REQUIRES_NEWを多用すると設計が読みにくくなります。さらに、接続プールへの負荷も増える可能性があります。そのため、使う場合は理由を明確にします。
外部APIを呼びすぎない
実務で注意したいのが、トランザクション内の外部API呼び出しです。DBロックを保持したまま外部APIの応答を待つと、処理時間が長くなります。
@Transactional
public void register(UserRequest request) {
User user = userRepository.save(request.toUser());
mailClient.sendWelcomeMail(user.email());
}
この例では、メール送信に時間がかかると、DBトランザクションも長く開いたままになります。また、メールは送れたのにDBだけrollbackされる、といった不整合も起こり得ます。
そのため、外部API、メール送信、メッセージ送信は分けることを検討します。トランザクション後の処理にする考え方です。イベント、キュー、補償処理などを使う設計も候補になります。
自己呼び出しに注意する
SpringのトランザクションアノテーションはAOPで実現されています。そのため、同じクラス内のメソッド呼び出しには注意が必要です。thisで呼び出すと、期待したトランザクションが適用されないことがあります。
public void importUsers(List<UserRequest> requests) {
for (UserRequest request : requests) {
saveOne(request);
}
}
@Transactional
public void saveOne(UserRequest request) {
userRepository.save(request.toUser());
}
このような自己呼び出しは、レビューで見落とされやすいポイントです。トランザクション境界を別Serviceに分ける方法があります。呼び出し構造を見直す対応も検討します。
Spring Boot トランザクション設計で避けたいアンチパターン
実務レビューでは、次のような設計を見つけたら注意します。
| アンチパターン | 起きやすい問題 | 見直すポイント |
|---|---|---|
| Repositoryごとにトランザクションを付ける | 業務処理全体の一貫性が見えにくい | Service層の業務単位で考える |
| rollback条件を理解せず例外を投げる | 想定外にcommitされる | checked exceptionとRuntimeExceptionを確認する |
| REQUIRES_NEWを多用する | 処理の一貫性が読みにくくなる | 独立させる理由を明確にする |
| 長い処理をトランザクション内に入れる | ロック時間が長くなる | 外部APIや重い処理を分離する |
| 自己呼び出しにトランザクションを期待する | アノテーションが効かないことがある | 別Service化や呼び出し構造を見直す |
もちろん、すべてを細かく分ければよいわけではありません。重要なのは、どこからどこまでを一貫して成功させたいのかを説明できることです。
実務で見るSpring Bootトランザクション設計のチェックリスト
- 業務処理として成功または失敗させたい範囲が明確か?
- トランザクションをService層の適切なメソッドに付けているか?
- rollbackされる例外とされない例外を把握しているか?
- readOnlyのメソッドで更新処理をしていないか?
- REQUIRES_NEWを使う理由が明確か?
- 外部APIやメール送信をトランザクション内で待っていないか?
- 自己呼び出しでトランザクションが効くと誤解していないか?
- 例外処理、ログ、監査、補償処理まで含めて設計しているか?
まずは、このチェックリストでトランザクション境界を見直します。そのうえで、例外設計や外部連携の扱いまで確認すると、障害時に説明しやすい設計になります。
Spring Bootトランザクション設計でよくある質問
Controllerに付けてもよいですか?
基本的にはService層に付ける方が自然です。ControllerはHTTPリクエストとレスポンスの入口です。一方で、トランザクションは業務処理の一貫性を守るためのものです。そのため、Service層で境界を表す方がレビューしやすくなります。
checked exceptionでもrollbackしたい場合は?
rollbackForを指定する選択肢があります。ただし、まずはその例外で本当にDB更新を戻すべきかを整理します。例外設計と業務ルールを確認したうえで、必要な場所にだけ指定します。
トランザクション設計でREQUIRES_NEWは監査ログに使うべきですか?
監査ログを本処理とは独立して残したい場合、候補になります。ただし、別トランザクションになるため設計意図を明確にする必要があります。接続プールや失敗時の扱いも確認します。
まとめ:Spring Bootトランザクション設計は業務境界を決める設計である
Spring Bootトランザクション設計では、トランザクションを付ける場所だけでは不十分です。業務処理の境界を見ます。rollback条件とpropagationも確認します。外部連携や自己呼び出しまで含めて設計する必要があります。
まずは、1つの業務処理として成功または失敗させたい範囲を決めます。次に、Service層にトランザクション境界を置きます。さらに、例外時のrollback方針を明確にします。この流れを守ると、レビューしやすく保守しやすい設計に近づきます。
Java/Spring Bootの実務では、この設計と例外処理がつながります。SQL設計や外部連携も切り離して考えることはできません。bluenaの採用情報では、Spring Boot経験を活かせる案件相談も行っています。今の経験を次の環境でどう伸ばすか、一度整理してみてください。
一度カジュアル面談をしませんか?
株式会社bluenaは「高還元」と「伴走支援」を両立したSES企業です。単価の81〜86%を還元する報酬体系と、専任サポーターによる隔週1on1で、エンジニアが納得できるキャリアを実現します。
まとまっていなくてもOK——まずは現在地を聞かせてください。
カジュアル面談ですので、お気軽にお聞かせください。





