はじめに
マイクロサービスにおける長時間処理や分散トランザクションは、障害時の整合性確保が難しい。Temporal.ioは「1トランザクション=1ワークフロー」の考え方で、耐久性のある実行を保証する。Claude Codeを使い、Sagaパターンと補償処理を中心に設計手順を紹介する。
1トランザクション=1ワークフロー設計
Temporalの基本思想は、1ビジネストランザクションを1つのワークフローとして表現することだ。ワークフローはクラッシュしても再開でき、状態はTemporalサーバーが永続化する。
import { proxyActivities, sleep } from '@temporalio/workflow';
import type { OrderActivities } from './activities';
const { reserveInventory, chargePayment, shipOrder } = proxyActivities<OrderActivities>({
startToCloseTimeout: '30 minutes',
retry: {
maximumAttempts: 3,
initialInterval: '1s',
backoffCoefficient: 2,
},
});
export async function orderWorkflow(orderId: string): Promise<void> {
// 1トランザクション = このワークフロー全体
await reserveInventory(orderId);
await chargePayment(orderId);
await shipOrder(orderId);
}
Claude Codeへの指示例:「orderWorkflowを定義して。各ステップはアクティビティとして分離し、タイムアウトとリトライポリシーを設定して」
Sagaクラスと補償配列(逆順実行)
障害発生時に実行済みステップを逆順で巻き戻すSagaパターンを実装する。compensations配列に補償関数を積み上げ、エラー時にreverse()して順番に実行する。
class Saga {
private compensations: Array<() => Promise<void>> = [];
addCompensation(fn: () => Promise<void>): void {
this.compensations.push(fn);
}
async compensate(): Promise<void> {
// 逆順で補償を実行(最後に実行したものから戻す)
const reversed = [...this.compensations].reverse();
for (const compensation of reversed) {
try {
await compensation();
} catch (err) {
// 補償失敗はログのみ(ベストエフォート)
console.error('Compensation failed:', err);
}
}
}
}
export async function orderWorkflowWithSaga(orderId: string): Promise<void> {
const saga = new Saga();
try {
await reserveInventory(orderId);
saga.addCompensation(() => releaseInventory(orderId));
await chargePayment(orderId);
saga.addCompensation(() => refundPayment(orderId));
await shipOrder(orderId);
// 出荷後の補償はビジネスルール次第
saga.addCompensation(() => cancelShipment(orderId));
} catch (err) {
await saga.compensate();
throw ApplicationFailure.nonRetryable(
`Order ${orderId} failed and compensated`,
'ORDER_COMPENSATION_COMPLETE',
);
}
}
冪等なアクティビティ設計(存在チェック)
Temporalはアクティビティを自動リトライする。そのため各アクティビティは冪等でなければならない。「既に処理済みか」を存在チェックで判定し、二重実行を防ぐ。
export const activities: OrderActivities = {
async reserveInventory(orderId: string): Promise<void> {
// 存在チェックで冪等性を保証
const existing = await db.query(
'SELECT id FROM inventory_reservations WHERE order_id = $1',
[orderId]
);
if (existing.rows.length > 0) {
// 既に予約済み → 正常終了(冪等)
return;
}
await db.query(
'INSERT INTO inventory_reservations (order_id, created_at) VALUES ($1, NOW())',
[orderId]
);
},
async chargePayment(orderId: string): Promise<void> {
const existing = await db.query(
'SELECT id FROM payment_charges WHERE order_id = $1',
[orderId]
);
if (existing.rows.length > 0) {
return; // 二重請求防止
}
await paymentGateway.charge(orderId);
await db.query(
'INSERT INTO payment_charges (order_id, charged_at) VALUES ($1, NOW())',
[orderId]
);
},
};
Claude Codeへの指示例:「reserveInventoryを冪等にして。order_idで既存レコードをチェックし、あれば早期リターンする実装にして」
ApplicationFailure.nonRetryableで補償完了を明示
Saga補償が完了した後は、これ以上リトライしないことをTemporalに伝える必要がある。ApplicationFailure.nonRetryableを使うことで、ワークフローを確実に失敗終了させる。
import { ApplicationFailure } from '@temporalio/workflow';
// 補償完了後 → nonRetryableで確実に失敗終了
throw ApplicationFailure.nonRetryable(
`Order ${orderId} saga compensated successfully`,
'SAGA_COMPENSATED',
{ orderId, compensatedAt: new Date().toISOString() }
);
// ワーカー側でエラー種別を判別可能
if (err instanceof ApplicationFailure && err.type === 'SAGA_COMPENSATED') {
await notifyOrderFailed(orderId);
}
リトライ可能なエラーとリトライ不可エラーを明確に分けることで、無限リトライループを防ぐ。
まとめ
- 1トランザクション=1ワークフロー:Temporalが状態を永続化するため、クラッシュしても再開できる
-
Sagaパターン:
compensations配列を逆順実行することで、分散トランザクションの巻き戻しを実現する - 冪等なアクティビティ:存在チェックで二重実行を防ぎ、安全なリトライを保証する
- ApplicationFailure.nonRetryable:補償完了後に明示的な失敗終了を指示し、無限ループを回避する
Claude Codeにアーキテクチャ方針を伝えれば、Saga実装・冪等チェック・エラー種別分岐まで一気に生成できる。
みょうがのCode Review Pack(¥980)では、Temporal・マイクロサービス設計レビュー用のClaude Codeカスタムスキルを提供しています。
Code Review Pack を見る →
Top comments (0)