transactional-patterns について
transactional-patterns は、アプリケーションコードで Spring トランザクションを扱うためのスキルです。既存の設計を保ちながら、伝播、ロールバック、並行処理、分散ワークフローに対応するコーディングエージェントを支援します。
主な機能
- 01トランザクション境界の選定
- 02伝播動作の理解
- 03ロールバックと例外の処理
- 04楽観ロックと再試行の適用
- 05Saga による分散ステップの調整
- 06コミット後の副作用と検証の管理
活用シーン
- 01サービス変更前のトランザクション注釈の確認
- 02検査例外のロールバック規則の実装
- 03新しいトランザクションでの楽観ロック競合への対応
- 04コミット後イベントの永続配信の計画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rrezartprebreza/ spring-boot-skills --skill transactional-patternsインストール後、Claude Code で/transactional-patternsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlexamples/bad-transactional-service.javagood-transactional-service.javatemplates/TransactionalOrderService.java詳細
トランザクションパターン
境界を明確にする
- 伝播設定を変更する前に、プロジェクトのBoot/Frameworkのバージョン、トランザクションマネージャー、永続化技術、ロールバック設定、呼び出し元を確認します。
- 複数ステップから成るビジネストランザクションは、アプリケーション/サービス境界に配置します。Spring Dataリポジトリのトランザクションは維持してください。リポジトリのアノテーションが本質的に誤りというわけではありません。
- デフォルトの伝播設定は
REQUIREDです。既存のトランザクションに参加するか、新しいトランザクションを作成します。 - 適切な読み取り単位の処理には
readOnly = trueを使用します。これは最適化のヒントであり、書き込みの禁止、認可、読み取りレプリカへの自動ルーティングを意味しません。 - 既存のアーキテクチャとトランザクションマネージャーの選択を維持します。ローカルデータベースのトランザクションによって、別のデータベースやHTTPプロバイダーへの書き込みがアトミックになるわけではありません。
以下のスニペットは説明用です。ドメインクラスとインポートはアプリケーション側で用意します。 サービスを編集する際は、サービスのテンプレートを適用し、適切な例と不適切な例を比較してください。
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // default for all methods in this service
public class OrderService {
@Transactional // overrides readOnly for writes
public Order createOrder(CreateOrderRequest request) {
inventoryService.reserve(request.items()); // participates in same TX
return orderRepository.save(Order.from(request));
}
public Optional<Order> findById(UUID id) {
return orderRepository.findById(id); // readOnly = true inherited
}
}
伝播設定
| 伝播設定 | 動作 |
|---|---|
REQUIRED(デフォルト) | 既存のトランザクションに参加するか、新しいトランザクションを作成します |
REQUIRES_NEW | 常に新しいトランザクションを作成し、既存のトランザクションを一時停止します |
SUPPORTS | トランザクションがあれば参加し、なければトランザクションなしで処理します |
NOT_SUPPORTED | 常にトランザクションなしで実行します |
MANDATORY | 既存のトランザクションが必須です。ない場合は例外をスローします |
NEVER | トランザクションがあってはなりません。存在する場合は例外をスローします |
参加するREQUIRED呼び出しでは、外側のトランザクションの分離レベル、タイムアウト、読み取り専用設定が使用されます。内側の処理で発生した失敗を捕捉しても、ロールバック専用マーカーは解除されません。外側のコミット時にもUnexpectedRollbackExceptionがスローされる可能性があります。
REQUIRES_NEWでは、外側のトランザクションがリソースを保持している間、別の接続が必要になります。接続プールの容量とロック競合を考慮してください。独立したコミットは、失敗した操作後も保持する必要がある記録(たとえば、試行された操作)のために確保し、ロールバックされた注文が作成されたと主張する成功監査レコードには使用しないでください。独立した監査トランザクションから、未コミットの親レコードを参照してはいけません。
// REQUIRES_NEW — for audit logging that must survive rollback
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditEvent(AuditEvent event) {
auditRepository.save(event); // commits independently of parent TX
}
// Illustrative: record an attempt for an existing order; failures are unchecked.
@Transactional
public void processOrder(Order order) {
auditService.logAuditEvent(new AuditEvent("ORDER_START", order.getId()));
try {
// ... process, may throw
} catch (RuntimeException e) {
auditService.logAuditEvent(new AuditEvent("ORDER_FAILED", order.getId()));
throw e; // parent TX rolls back, audit TX already committed
}
}
自己呼び出しの落とし穴
// ❌ BROKEN — self-invocation bypasses Spring proxy, @Transactional ignored
@Service
public class OrderService {
@Transactional
public void processAll(List<UUID> ids) {
ids.forEach(id -> this.processSingle(id)); // bypasses proxy!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processSingle(UUID id) { ... } // never creates new TX
}
// ✅ FIX — extract the independently transactional operation to another bean
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderProcessor orderProcessor; // separate bean
@Transactional
public void processAll(List<UUID> ids) {
ids.forEach(id -> orderProcessor.processSingle(id)); // goes through proxy
}
}
例外の処理
// Default: RuntimeException and Error roll back; checked exceptions do not.
// Check project-wide rollback configuration before adding per-method rules.
@Transactional(rollbackFor = InsufficientInventoryException.class) // checked exception
public Order createOrder(CreateOrderRequest request) throws InsufficientInventoryException {
...
}
OptimisticLockExceptionから回復するために、noRollbackForを決して使用しないでください。永続化プロバイダーはトランザクションをロールバック専用としてマークします。ロールバックして、上限のあるポリシーと新しく取得したエンティティを使い、処理単位全体を新しいトランザクションで再試行してください。
楽観的ロック
@Entity
public class Order {
@Version
private Long version; // Hibernate handles conflicts automatically
}
// Handles concurrent updates
@Transactional
public Order updateStatus(UUID id, OrderStatus newStatus) {
Order order = orderRepository.findById(id).orElseThrow();
order.updateStatus(newStatus); // conflict can surface at flush or commit
return orderRepository.save(order);
}
依存関係とアノテーションのインポートについては、バージョン別の再試行ガイダンスを使用してください。再試行を行うファサードがすでにトランザクションを保持していると、各試行のREQUIREDメソッドがそのトランザクションに参加してしまうため、注意してください。
分散トランザクション(Sagaパターン)
Sagaは永続化されたローカルステップを調整する仕組みです。リモート呼び出しを@Transactionalで囲んでも、Sagaは実装されません。
- 保留中の注文、Sagaの状態、送信コマンドを1つのローカルトランザクションでコミットします。
- 安定したプロバイダーの冪等性キーを使用し、そのトランザクションの外でコマンドをディスパッチします。
- 確認された各結果と次のコマンドを、別のローカルトランザクションでアトミックに永続化します。
- 拒否された場合は、失敗状態と補償コマンドを永続化します。補償は独立して再試行し、リモート側の結果が不明な場合はタイムアウトを調整します。
失敗状態を保存した後、同じトランザクション内でロールバックを引き起こす例外をスローしないでください。状態が消去されてしまいます。クラッシュ後の復旧は、永続化された状態から再開できる必要があります。プロジェクトで既に使われているワークフローの仕組みを使用してください。配送と重複による影響については、メッセージング/アウトボックスおよび冪等性を参照してください。
コミット後の副作用
ベストエフォートのローカル通知では、コミットに紐づくリスナーを使用することで、ロールバック時の配信を防げます。
確実な配信が必要な場合は、業務上の変更とともにアウトボックスエントリーを永続化するか、プロジェクトの永続的なパブリケーション機構を使用してください。AFTER_COMMITだけでは、プロセスのクラッシュから復旧したり、配信の失敗を再試行したりできません。
// Publisher — inside the TX
@Transactional
public Order place(UUID id) {
Order order = orderRepository.findById(id).orElseThrow();
order.place();
eventPublisher.publishEvent(new OrderPlaced(order.getId())); // published now; listener defers handling
return orderRepository.save(order);
}
// Listener — runs ONLY if the TX commits successfully
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPlaced(OrderPlaced event) {
emailService.sendConfirmation(event.orderId()); // best effort; no durable retry
}
AFTER_COMMITはDBのコミット後に実行されますが、リソースは完了したトランザクションに紐づいたままの場合があります。データベースへの書き込みは、別のREQUIRES_NEW Beanに委譲してください。リスナーが失敗しても、コミット済みの業務上の書き込みは取り消せません。配信の失敗を把握でき、復旧可能にしてください。
アクティブなトランザクションがない場合、デフォルトではリスナーはスキップされます。@Asyncを使用しても、配信は永続化されません。ドメインイベントを参照してください。
検証
Springプロキシ経由でテストし、新しいトランザクションからコミット済みデータを検証してください。チェック例外と非チェック例外の違い、内部でのロールバック専用伝播、コミット時の楽観的競合、監査のロールバック、コミット後とロールバック後のリスナーの動作を網羅してください。分離レベルと競合のテストには、本番環境と同じデータベースエンジンを使用してください。テスト全体のロールバックによって、コミット後の動作が隠れることがあります。
注意点
- エージェントがリポジトリのトランザクションアノテーションを削除する — 作業単位と既存のSpring Dataの動作を維持してください。
- エージェントが
AFTER_COMMITを永続的なメッセージングとして扱う — 確実な配信が必要な場合は、アウトボックスまたは永続的なパブリケーションレジストリを使用してください。 - エージェントがレプリカへのルーティングや書き込み保護に
readOnlyを使用する — フラグだけでは、どちらも実現されません。 - エージェントが
REQUIRES_NEWで成功監査エントリーをコミットする — 業務上の変更とともにコミットしてください。 - エージェントが楽観的ロックによるロールバックを抑制する — 新しいトランザクションで再試行してください。
- エージェントが
this上で@Transactionalメソッドを呼び出す — 自己呼び出しではプロキシを経由しません。 - エージェントがチェック例外でロールバックすると想定する —
rollbackForを追加する必要があります。 - エージェントが
privateメソッドに@Transactionalを使用する — Springプロキシではインターセプトできません。