この記事の要点
• Sagaパターンはローカルトランザクションの連鎖と補償トランザクションで分散トランザクションを実現
• コレオグラフィ(イベント駆動)とオーケストレーション(中央制御)の2方式がある
• 冪等性の確保・Sagaログの永続化・補償ロジックの設計が成功の鍵
Saga パターンとは
Saga パターンは、複数のサービスにまたがる長期的な業務トランザクションを、ローカルトランザクションの連鎖として実現する設計パターンです。1987 年に Hector Garcia-Molina と Kenneth Salem の論文「Sagas」で提案され、近年マイクロサービスアーキテクチャの普及とともに再注目されました。
各ステップは独立したローカルトランザクションとして実行され、失敗した場合は「補償トランザクション(Compensating Transaction)」によってこれまでの操作を打ち消します。
sequenceDiagram
participant Order as 注文サービス
participant Pay as 決済サービス
participant Inv as 在庫サービス
participant Ship as 配送サービス
Order->>Pay: 決済実行
Pay-->>Order: 成功
Order->>Inv: 在庫引当
Inv-->>Order: 成功
Order->>Ship: 配送手配
Ship-->>Order: 失敗
Order->>Inv: 在庫引当キャンセル(補償)
Order->>Pay: 返金(補償)
なぜ Saga パターンが必要か
2 フェーズコミット(2PC)の限界
伝統的な分散トランザクションは 2 フェーズコミットで実現されてきましたが、マイクロサービスとは相性が悪い問題があります。
- すべての参加者がロックを保持し続けるためスケーラビリティが低い
- 1 つのサービスがダウンすると全体がブロックされる
- 異種データベース(RDB と NoSQL とメッセージブローカー)を跨ぐ実装が困難
- HTTP/REST のような疎結合プロトコルとは相性が悪い
マイクロサービスにおける整合性
マイクロサービスでは「サービスごとにデータベースを持つ(Database per Service)」原則があり、ACID トランザクションを跨らせることはできません。Saga パターンは「結果整合性(Eventual Consistency)」を受け入れつつ、業務的に意味のある整合性を保ちます。
基本原則
ローカルトランザクションの連鎖
Saga は複数のローカルトランザクションを順番に実行します。各ローカルトランザクションは自分のデータベースに対する ACID 操作です。
補償トランザクション
ある段階で失敗した場合、これまで成功したステップを「論理的に取り消す」補償トランザクションを実行します。物理的にロールバックするのではなく、業務的に逆操作を行います。
結果整合性
全ステップが完了するまでの間、システムは一時的に不整合な状態を持ち得ます。利用者から見える整合性は最終的に保証されます。
冪等性
同じステップが複数回実行されても結果が変わらないように、すべての操作は冪等に設計します。
構成要素の詳細
Saga には 2 種類ある
コレオグラフィ(Choreography)
中央の調整役を持たず、各サービスがイベントを発行し、他のサービスがそれを購読して動作します。サービス間がイベントブローカー経由で疎結合に連鎖します。
flowchart LR
O["Order Service"] -->|OrderCreated| B((Event Bus))
B -->|OrderCreated| P["Payment Service"]
P -->|PaymentCompleted| B
B -->|PaymentCompleted| I["Inventory Service"]
I -->|InventoryReserved| B
B -->|InventoryReserved| S["Shipping Service"]
オーケストレーション(Orchestration)
中央の Saga オーケストレータが各サービスを順番に呼び出し、結果に応じて次の動作を決めます。フローが明示的で追跡しやすいです。
flowchart TB
SO["Saga Orchestrator"]
SO -->|1.決済| P["Payment Service"]
SO -->|2.在庫引当| I["Inventory Service"]
SO -->|3.配送| S["Shipping Service"]
補償ロジック
各成功ステップには対応する補償ステップがあります。たとえば「決済」に対しては「返金」、「在庫引当」に対しては「在庫解放」、「メール送信」に対しては「キャンセル通知」など。
Saga ログ
実行状況を永続化することで、障害復旧時に途中から再開できるようにします。Event Sourcing と組み合わせることが多いです。
実装例(オーケストレーション・TypeScript 擬似コード)
Saga 定義
type StepResult = "SUCCESS" | "FAILURE";
interface SagaStep {
name: string;
execute(ctx: SagaContext): Promise<StepResult>;
compensate(ctx: SagaContext): Promise<void>;
}
interface SagaContext {
orderId: string;
amount: number;
items: Array<{ sku: string; qty: number }>;
state: Record<string, unknown>;
}
オーケストレータ
export class SagaOrchestrator {
constructor(private readonly steps: SagaStep[]) {}
async run(ctx: SagaContext): Promise<void> {
const completed: SagaStep[] = [];
for (const step of this.steps) {
try {
const result = await step.execute(ctx);
if (result === "FAILURE") {
await this.compensate(completed, ctx);
throw new Error(`Step ${step.name} failed`);
}
completed.push(step);
} catch (err) {
await this.compensate(completed, ctx);
throw err;
}
}
}
private async compensate(
completed: SagaStep[],
ctx: SagaContext,
): Promise<void> {
for (const step of completed.reverse()) {
try {
await step.compensate(ctx);
} catch (err) {
console.error(`Compensation failed for ${step.name}`, err);
}
}
}
}
各ステップの実装
const paymentStep: SagaStep = {
name: "payment",
async execute(ctx) {
const ok = await paymentService.charge(ctx.orderId, ctx.amount);
return ok ? "SUCCESS" : "FAILURE";
},
async compensate(ctx) {
await paymentService.refund(ctx.orderId);
},
};
const inventoryStep: SagaStep = {
name: "inventory",
async execute(ctx) {
const ok = await inventoryService.reserve(ctx.orderId, ctx.items);
return ok ? "SUCCESS" : "FAILURE";
},
async compensate(ctx) {
await inventoryService.release(ctx.orderId);
},
};
const shippingStep: SagaStep = {
name: "shipping",
async execute(ctx) {
const ok = await shippingService.schedule(ctx.orderId);
return ok ? "SUCCESS" : "FAILURE";
},
async compensate(ctx) {
await shippingService.cancel(ctx.orderId);
},
};
const saga = new SagaOrchestrator([paymentStep, inventoryStep, shippingStep]);
await saga.run({
orderId: "ord-001",
amount: 5000,
items: [{ sku: "SKU-A", qty: 1 }],
state: {},
});
メリット
- 疎結合: サービスごとに独立したデータベースを保てる
- スケーラビリティ: 長時間ロックが発生しない
- 可用性: 一部の障害が全体を止めない
- 異種データストア対応: RDB、NoSQL、メッセージブローカーを混在可能
- 業務的明示性: 補償ロジックを設計することで業務要件が明確になる
デメリット
- 設計の複雑さ: 補償ロジックを全パターン用意する必要がある
- デバッグの難しさ: 分散したログを追わないと挙動が分からない
- 結果整合性の許容: 即時整合性を要求する業務には不向き
- 補償不能な操作の問題: 物理的なメール送信や外部 API 呼び出しは「取り消せない」
- テストの困難さ: 全パスの組み合わせ爆発
ユースケース
- EC の注文処理: 決済・在庫・配送・通知を跨ぐワークフロー
- 旅行予約システム: ホテル・航空券・レンタカーの一括予約
- 金融取引: 振込・口座更新・通知の組み合わせ
- ユーザー登録: 認証サービス・課金サービス・通知サービスの一括処理
- データ同期: 複数システム間のマスターデータ伝播
よくある落とし穴
注意: メール送信や外部API呼び出しなど、物理的に取り消せない操作はSagaの最終ステップに配置してください。
補償ができない操作
メール送信や外部決済は物理的に取り消せません。可能な限り「最終ステップ」に置く、または「キャンセル通知」のような事後対応で代替します。
冪等性の欠如
ネットワーク再試行で同じステップが二度実行されると、二重課金などの致命的問題が発生します。リクエスト ID やトランザクション ID で重複検知します。
補償の連鎖失敗
補償ステップ自体が失敗することがあります。ログに残してデッドレターキューに送り、運用で対応する仕組みが必要です。
Saga ログを永続化していない
オーケストレータがクラッシュすると途中状態が失われます。各ステップの完了状態を必ず永続化します。
過剰なオーケストレーション
オーケストレータに業務ロジックを集中させすぎると、新たな密結合が生まれます。Saga はワークフロー制御に徹し、業務判断は各サービスに委ねます。
他パターンとの比較
| 観点 | Saga | 2 フェーズコミット | TCC (Try-Confirm-Cancel) |
|---|---|---|---|
| 整合性 | 結果整合性 | 強整合性 | 強整合性に近い |
| ロック | 短時間のローカルのみ | 長時間の分散ロック | 試行段階で予約 |
| 可用性 | 高 | 低 | 中 |
| 実装難易度 | 中〜高 | 低(DB が対応すれば) | 高 |
| マイクロサービス適合 | 高 | 低 | 中 |
| 補償の必要性 | あり | なし | あり |
ベストプラクティス
1. 業務単位で Saga を区切る
ひとつの Saga が長すぎるとデバッグも補償設計も破綻します。意味のある業務単位で区切ります。
2. 冪等性を最優先に設計
各ステップは何度呼ばれても同じ結果になるよう設計します。リクエスト ID を必ず付与します。
3. 状態の可視化
Saga の現在ステップ、成功・失敗状況をダッシュボードで見えるようにします。
4. 補償できない操作を後ろに置く
メール送信のような不可逆操作は最終ステップに配置し、補償の必要をなくします。
ポイント: ステップ数が少なく独立性が高ければChoreography、複雑で追跡性が重要ならOrchestrationを選びましょう。
5. Choreography と Orchestration の使い分け
ステップ数が少なく独立性が高ければ Choreography、複雑で追跡性が重要なら Orchestration を選びます。
6. デッドレターキューを用意
補償も失敗した場合の最終的な受け皿を用意し、運用で介入できるようにします。
まとめ
Saga パターンは、マイクロサービス時代における分散トランザクションの現実解です。ACID を諦める代わりに、結果整合性と業務的な補償ロジックでデータ整合性を保ちます。設計には熟慮が必要ですが、正しく設計された Saga はスケーラブルで可用性の高いシステムを実現します。コレオグラフィとオーケストレーションを使い分け、冪等性と観測性を担保することが成功の鍵です。
実践メモ: Temporal、AWS Step Functions、Camundaなどのワークフローエンジンを活用すると、Sagaの永続化・再実行・補償を組み込みでサポートしてくれます。
さらに踏み込んだトピック
Saga と Process Manager の関係
オーケストレーション型 Saga はしばしば「Process Manager」と呼ばれます。この用語は Enterprise Integration Patterns で登場した、長期実行ワークフローを管理する責務オブジェクトを指します。Saga オーケストレータと Process Manager はほぼ同義で使われることが多いですが、Process Manager の方がより汎用的なメッセージングの抽象として位置づけられます。
イベントソーシングとの相性
Saga の実行履歴を Event Sourcing で保存すると、いつどのステップが完了したかが時系列で追跡できます。クラッシュからの再開、デバッグ、監査証跡の生成が容易になります。
interface SagaEvent {
sagaId: string;
timestamp: Date;
type: "STEP_COMPLETED" | "STEP_FAILED" | "COMPENSATED";
step: string;
payload: Record<string, unknown>;
}
タイムアウトの設計
Saga の各ステップにはタイムアウトを設定すべきです。応答を永遠に待つと進行が止まります。タイムアウト発生時は失敗とみなして補償フェーズに入りますが、後から本来の応答が遅れて到着するケースに備え、補償後のメッセージは破棄するロジックも必要です。
Pivot Step と Retriable Step
Caitie McCaffrey の提唱に基づくと、Saga のステップは大きく 3 種類に分けられます。
- Compensatable Step: 失敗時に補償可能な操作
- Pivot Step: ここを越えると後戻りできない決定的操作
- Retriable Step: 必ず成功するまで再試行する操作(通常は Pivot 後に配置)
Pivot Step の前に Compensatable Step を、後に Retriable Step を置くという順序設計が安全な Saga の基本構造です。
観測性とデバッグ
Saga はまたがるサービスが多いため、分散トレーシングを必須で組み込むべきです。trace_id を Saga ID と紐づけ、Jaeger や Grafana Tempo で全体像を可視化することで、複雑な失敗パターンの調査時間が大幅に短縮されます。
State Machine Visualization
Saga の状態遷移を YAML/DSL で記述し、ダイアグラムとして自動生成する方式も普及しています。AWS Step Functions、Temporal、Camunda などのワークフローエンジンが代表例です。これらは Saga の永続化・再開・タイムアウト・補償を組み込みでサポートします。
# 簡易的な Saga DSL の例
saga: order-fulfillment
steps:
- name: charge_payment
on_failure: stop
- name: reserve_inventory
on_failure: refund_payment
- name: schedule_shipping
on_failure: [release_inventory, refund_payment]
実運用上の追加トピック
Saga ID と冪等性キー
各 Saga 実行には一意な Saga ID を付与し、すべてのステップ呼び出しに伝播させます。各サービスは「同じ Saga ID と同じステップの組み合わせで二度目の呼び出しが来たら、結果のみ返す」という冪等な実装にします。これにより、ネットワーク再試行やオーケストレータのクラッシュリカバリ後の再実行で副作用が二重発生するのを防ぎます。
失敗時のロールバック範囲
すべての失敗で全ステップを補償するとは限りません。たとえば「配送手配が翌日に再試行可能なら待つ」「決済はすでに確定しているので返金しない」など、業務ルールによって補償の判断は変わります。ステップごとに「失敗時のポリシー」を明示的に定義しましょう。
Outbox パターンとの組み合わせ
サービスがローカル DB の更新とイベント発行の両方を行う場合、両者がアトミックに行われないと不整合が生まれます。Outbox パターンでは、DB トランザクション内で「業務テーブル」と「outbox テーブル」を同時更新し、別プロセスが outbox を読み取って実際にイベントを発行します。Saga と組み合わせることで、ステップの結果イベントが必ず発行される保証が得られます。
BEGIN;
UPDATE orders SET status = 'PAID' WHERE id = 'ord-001';
INSERT INTO outbox(event_type, payload, created_at)
VALUES ('OrderPaid', '{"orderId":"ord-001"}', now());
COMMIT;
Saga の可観測性
Saga ID をすべてのログとトレースに付与することで、1 つの業務トランザクションの全貌を追跡できます。Jaeger・Tempo などで分散トレースとして可視化すれば、どのステップで時間がかかっているか、どのステップが頻繁に補償されているかが一目瞭然です。
Workflow Engine の活用
自前で Saga を実装するのは煩雑で誤りやすい作業です。次の Workflow Engine は Saga の永続化、再実行、補償を組み込みでサポートしています。
| エンジン | 特徴 |
|---|---|
| Temporal | コードファーストで耐久性のあるワークフロー定義 |
| AWS Step Functions | マネージド、ASL(Amazon States Language)で記述 |
| Camunda | BPMN 2.0 ベース、業務関係者が読めるダイアグラム |
| Cadence | Uber 発、Temporal の前身 |
| Netflix Conductor | JSON DSL、マイクロサービス志向 |
業務的な要件(監査・可視化・人間の介入など)に合わせて選定します。
Saga と CQRS の連携
書き込み(コマンド)側を Saga で実行し、読み取り(クエリ)側を別のリードモデルで構築することで、複雑な集約結果を高速に提供できます。Saga が発行するイベントを購読してリードモデルを更新する流れは Event Sourcing + CQRS + Saga の典型構成です。
ケーススタディ
ケース 1: ECサイトの注文ワークフロー
典型的な注文 Saga は次のステップで構成されます。
- 在庫予約(Inventory Reserve)
- 決済承認(Payment Authorize)
- 注文確定(Order Confirm)
- 配送手配(Shipping Schedule)
- 顧客通知(Notify Customer)
ステップ 4 で配送手配に失敗した場合、補償として「決済キャンセル」「在庫解放」「注文取消」を逆順に実行します。ステップ 5 のメール送信は補償できないため、可能な限り Saga の最後に配置します。
ケース 2: 旅行予約の同時進行
ホテル・航空券・レンタカーを同時予約するケースでは、3 つを並行に試行できます。すべて成功すれば確定、どれか一つでも失敗すれば成功した予約をすべてキャンセルします。並列 Saga は実装が複雑になりますが、ユーザー体験は向上します。
async function bookTrip(trip: TripRequest): Promise<void> {
const tasks = [
bookHotel(trip),
bookFlight(trip),
bookCar(trip),
];
const results = await Promise.allSettled(tasks);
const failed = results.some((r) => r.status === "rejected");
if (failed) {
await Promise.allSettled([
cancelHotel(trip),
cancelFlight(trip),
cancelCar(trip),
]);
throw new Error("Trip booking failed; all reservations cancelled");
}
}
ケース 3: ユーザー登録の段階的処理
ユーザー登録は「認証アカウント作成 → プロファイル登録 → 課金プラン登録 → ウェルカムメール送信」のように複数サービスを跨ぎます。途中で失敗した場合、認証アカウントを論理削除し、プロファイルを削除します。ウェルカムメールは送ってしまうと取り消せないため、最終ステップに置きます。
マイクロサービス境界の見直しサイン
Saga の補償ロジックが過剰に複雑になる場合、それはサービス境界の切り方が業務と一致していないサインかもしれません。「いつも一緒に変更される」「補償が頻繁に必要になる」サービスは、本来一つにまとめるべき可能性があります。Saga の複雑さを境界設計の品質指標として使うことができます。
設計時のチェックリスト
- 各ステップは冪等に実装されているか
- Saga ID を全ステップに伝播しているか
- 補償ロジックがすべてのステップに対して定義されているか
- 補償できない操作は最終ステップに配置されているか
- Saga 状態は永続化され、クラッシュからの再開が可能か
- タイムアウトが各ステップに設定されているか
- 補償の失敗時のフォールバック経路が設計されているか
- メトリクスとトレースで実行状況を可視化できるか
- テストで全ての成功・失敗パターンを網羅しているか
- 業務関係者と補償ポリシーが合意されているか
これらを満たすことで、本番運用に耐える Saga が実現できます。
参考リソース
- Microservices.io - Saga pattern
- Microsoft Azure Architecture Center - Saga distributed transactions pattern
- Sagas (Garcia-Molina, Salem 1987 - Original Paper)
- Martin Fowler - What do you mean by Event-Driven