ANTIGRAVITY LABEN
記事一覧/連携・プラグイン
連携・プラグイン/2026-06-14上級

Temporal の耐障害性ワークフローを本番で信用するために — 冪等性・リトライ分類・Saga 補償の実装メモ

Temporal を本番のバックエンドに据えてから見えてきた実装の勘所をまとめました。アクティビティの冪等性、リトライすべきエラーの線引き、Saga の補償が中途半端に効く事故、可観測性の設計を、Antigravity での開発手順とともに整理します。

Temporalワークフロー13冪等性4SagaリトライAntigravity233分散システム

プレミアム記事

きっかけは、深夜に二重課金の問い合わせが来たことでした

決済とプロビジョニングをまたぐ処理を Temporal に載せ替えて運用を始めた数週間後、「同じ請求が二回来ている」という問い合わせを受け取りました。ログを追うと、決済アクティビティがタイムアウト判定されてリトライされ、実際には一回目も成功していた、という典型的なパターンでした。

Temporal は「ワークフローのコードそのものを永続的な実行状態として扱う」ことで、ワーカーが落ちても正確な地点から再開してくれます。ただ、この再開の強力さは諸刃でもあります。アクティビティは設計次第で平気で複数回実行されますし、そのことを忘れた瞬間に副作用が二重に走ります。

ここでは、Temporal を本番運用に据えてから実際につまずいた箇所を中心に、冪等性・リトライの線引き・Saga の補償・可観測性という四つの観点で実装の勘所を整理します。入門記事ではなく、すでに動かしている人が「ここで事故った」を減らすためのメモとして読んでいただければ幸いです。開発自体は Antigravity のエージェントに任せる前提で、人間が握っておくべき判断はどこかも添えていきます。

「最低1回」を前提に置くと設計が変わります

Temporal のアクティビティに対してよく言われるのが at-least-once、つまり「最低1回は実行される」という保証です。「ちょうど1回」ではありません。タイムアウト、ワーカーのクラッシュ、ネットワークの一過性の失敗——どれが起きても、Temporal はアクティビティをもう一度呼びます。

ここで大事なのは、リトライが「失敗したから」だけでなく「成功したのに結果が返ってこなかったから」起きるという点です。決済 API が処理を完了したのにレスポンスが返る前に接続が切れれば、Temporal から見れば失敗で、もう一度叩きにいきます。だからこそ、副作用を持つアクティビティはすべて冪等であることが前提になります。

冪等化の手段は副作用の種類で変わります。自分の DB への書き込みなら一意制約と ON CONFLICT で吸収できますし、外部 API なら相手の冪等キー機能に乗るのが確実です。

// src/temporal/activities/billing.ts
import { ApplicationFailure } from '@temporalio/activity';
import { db, charges } from '../../db';
import { stripe } from '../../lib/stripe';
 
interface ChargeInput {
  orderId: string;   // ワークフロー側で確定済みの安定したID
  customerId: string;
  amount: number;
  currency: string;
}
 
/**
 * 課金アクティビティ
 * 冪等性は2層で担保する:
 *   1) Stripe の idempotencyKey で「同一リクエストは1度だけ処理」を相手側に保証させる
 *   2) 自分の charges テーブルにも結果を一意キーで記録し、再実行時は記録を正とする
 */
export async function chargeCustomer(input: ChargeInput): Promise<string> {
  const { orderId, customerId, amount, currency } = input;
 
  // 先に自分側の記録を確認する。すでに成功記録があれば API を叩かず返す
  const existing = await db.query.charges.findFirst({
    where: (c, { eq }) => eq(c.orderId, orderId),
  });
  if (existing?.status === 'succeeded') {
    return existing.stripeChargeId;
  }
 
  // orderId をそのまま冪等キーにする。リトライしても Stripe 側で二重課金されない
  const intent = await stripe.paymentIntents.create(
    { amount, currency, customer: customerId, confirm: true },
    { idempotencyKey: `charge-${orderId}` },
  );
 
  if (intent.status !== 'succeeded') {
    // ここはビジネス的な失敗。リトライしても結果は変わらないので非リトライにする
    throw ApplicationFailure.create({
      message: `決済が完了しませんでした: ${intent.status}`,
      type: 'PaymentNotCompleted',
      nonRetryable: true,
    });
  }
 
  await db
    .insert(charges)
    .values({ orderId, stripeChargeId: intent.id, status: 'succeeded', amount })
    .onConflictDoNothing();
 
  return intent.id;
}

この書き方で効いているのは、冪等キーに「ワークフロー側で確定した安定した ID」を使っている点です。アクティビティ内で crypto.randomUUID() を呼んでキーにすると、リトライのたびに値が変わって冪等性が崩れます。キーになる ID は必ずワークフロー本体で確定させ、引数として渡してください。Temporal のワークフローは決定的に再実行されるので、ワークフロー内で生成した ID はリトライをまたいでも同じ値になります。

冒頭の二重課金は、まさにこの「自分側の記録を先に見る」一手が抜けていたために起きました。Stripe の冪等キーだけに頼っていて、キーの生成がアクティビティ内にあったのが直接の原因でした。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
「最低1回実行される」前提でアクティビティを冪等化する具体的な書き方と、外部APIの冪等キーを使い分ける判断基準
リトライしてよいエラーと、即座に止めるべきエラーを型で分類し、nonRetryableErrorTypes で運用する設計
Saga の補償が部分的に走った時に二重ロールバックを防ぐ、補償の冪等化と OpenTelemetry での追跡方法
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

連携・プラグイン2026-04-25
Antigravity × Obsidian — コードと知識を同じ場所で育てるワークフロー
Antigravity で書いたコードを、Obsidian の知識ベースと結びつける実践ワークフロー。AIが生成したアーキテクチャ決定を記録し、次のセッションでその知識を再活用する仕組みを紹介します。
連携・プラグイン2026-06-09
壁紙アプリの画像配信を解像度バケット方式で組み直す — Antigravity エージェントに変換と検証を任せた運用設計
端末解像度が増えるたびに壁紙アプリの配信は重くなります。1枚の原画を全端末へ配る方式をやめ、解像度バケット+WebP/AVIF+エッジリダイレクトに組み直し、変換と検証を Antigravity エージェントに任せた運用設計を、実際のコードと閾値ごと残しました。
連携・プラグイン2026-06-02
Antigravity が self-signed certificate in chain で接続できないときの原因と対処
社内プロキシやセキュリティソフトの TLS 検査が入った環境で、Antigravity が self-signed certificate in chain や unable to verify the first certificate を出してモデルに接続できない症状の原因と直し方をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →