場当たり的なロジックから耐久性のある実行モデルへ
誰もが一度は経験したことがあるはずです。分散システムで長時間実行するプロセスを管理しようとすると、最初はシンプルなsetTimeoutや基本的なcronジョブから始まります。バックグラウンドタスクにBullMQを使うこともあるでしょう。しかし、30日間のユーザーオンボーディングシーケンスや高度決済パイプラインなど、ビジネスロジックが複雑化するにつれ、こうした自作ソリューションは崩壊し始めます。気づけば、本来の機能開発よりもリトライと状態永続化のための「接着剤コード」を書くことに時間を費やしていることになります。
従来のアプローチ vs 耐久性のある実行モデル
標準的なNode.js環境では、サーバーのクラッシュはメモリ上の状態にとって死を意味します。すべての進捗マーカーを手動でデータベースに保存していない限り、そのプロセスは消えてしまいます。StripeやTwilioなどの外部APIがダウンすれば、カスタムの指数バックオフロジックを書き、デッドレターキューを監視するだけで精一杯になります。
Temporalはこの問題を耐久性のある実行(Durable Execution)という概念で根本から覆します。状態をあなたが細かく管理する代わりに、Temporalがコードの各ステップを記録します。ワーカープロセスがクラッシュしても、Temporalは別のマシン上で処理を再開します。ローカル変数とスタックは、中断した時点の状態のまま完全に復元されます。バックエンド全体のアーキテクチャに「セーブポイント機能」が備わるようなイメージです。
| 機能 | 従来型(キュー+DB) | Temporalのアプローチ |
|---|---|---|
| 状態追跡 | 各ステップで手動UPDATEクエリ |
自動かつ透過的 |
| リトライ | 脆弱なカスタムコードのループ | 宣言的で堅牢なポリシー |
| タイムアウト | 週単位の追跡が悪夢になる | 数ヶ月単位のスリープをネイティブサポート |
| 可視性 | カスタム管理ダッシュボードを自作 | 完全な実行履歴付きのOOTB UI |
Temporalを導入する際のリアルな話
Temporalは単なるライブラリではなく、コードの考え方そのものを根本から変えるものです。大きな頭痛の種を解決してくれる一方で、チームが習得すべき独自のルールセットも存在します。
気に入るであろう点
- 盤石な信頼性:ワークフローは一時的な障害に対してほぼ無敵になります。ワーカーが落ちても、別のワーカーが1ミリ秒の進捗も失わずにバトンを引き継ぎます。
- 直線的なコード:シンプルなスクリプトのように見えるコードを書けます。「10行目と11行目の間で電源が落ちたらどうなるか?」と心配する必要はもうありません。
- タイムトラベル:テスト環境では時間をモックできます。30日間の請求サイクルを200ミリ秒未満で検証できます。
トレードオフ
- インフラのオーバーヘッド:Temporalクラスターの管理が必要です。データベース(PostgresまたはCassandra)とElasticsearchのようなインデックスエンジンが必要になります。
- 決定論のルール:これが最大のポイントです。ワークフローのコードは決定論的でなければなりません。Workflow内で
Math.random()、new Date()、直接のfetch()呼び出しは使えません。これらは「Activity」の中に置く必要があります。 - 学習曲線:オーケストレーション(Workflow)と実行(Activity)の分離を理解するために、チームには1〜2週間の慣れが必要です。
本番環境に耐えうるアーキテクチャ
数千の同時ワークフローを処理するシステムには、疎結合な構造を推奨します。トリガーと実行を分離することで、高負荷下でもAPIの応答性を維持できます。
- Temporalクラスター:処理の中枢です。Dockerで自己ホストするか、Temporal Cloudにメンテナンスを任せることができます。
- ワーカー:専用のNode.jsプロセスです。HTTPリクエストは処理せず、Temporalサーバーにタスクをポーリングして実行するだけです。
- クライアント:既存のExpressやFastify APIです。その役割は「ユーザーXに対してこのワークフローを開始してください」とTemporalに伝えるだけです。
TypeScriptはここでは必須です。Node.js SDKは高度な型マッピングを使用して、クライアントとワーカーの同期を保ち、本番環境に到達する前にバグを検出します。
実践:サブスクリプションエンジンの構築
実際のシナリオを見てみましょう。顧客に$29.99を請求し、ウェルカムメールを送信する必要があります。ネットワークの不具合で支払いが失敗した場合はリトライし、5回失敗したら担当者にエスカレーションします。
ステップ1:Activityのコーディング
ActivityはシステムのワーカーでありAPI呼び出しやデータベースへの書き込みなど、現実世界の複雑な処理を担当します。
// activities.ts
export async function processPayment(amount: number): Promise<string> {
// 実際のアプリではStripeのSDKを呼び出す
if (Math.random() < 0.1) throw new Error("上流ゲートウェイタイムアウト");
return "CHARGED_SUCCESSFULLY";
}
export async function sendWelcomeEmail(email: string): Promise<void> {
console.log(`メールを送信しました: ${email}`);
}
ステップ2:ワークフローのオーケストレーション
ここが魔法の場所です。裏側で複雑なリトライロジックを処理しているにもかかわらず、コードがいかにクリーンで順次的に見えるかに注目してください。
// workflows.ts
import { proxyActivities, sleep } from '@temporalio/workflow';
import type * as activities from './activities';
const { processPayment, sendWelcomeEmail } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
retry: {
initialInterval: '2s',
maximumAttempts: 5,
backoffCoefficient: 2,
},
});
export async function subscriptionWorkflow(email: string, amount: number): Promise<void> {
const status = await processPayment(amount);
if (status === 'CHARGED_SUCCESSFULLY') {
await sendWelcomeEmail(email);
}
// このスリープは30日間続く可能性がある
// ワーカーが100回再起動してもTemporalはこのタイマーを忘れない
await sleep('30 days');
}
ステップ3:ワーカーの起動
ワーカーはクラスターに接続し、作業を待ちます。分散システムの機関室といえる存在です。
// worker.ts
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'billing-v1',
});
await worker.run();
}
run().catch((err) => {
console.error("ワーカーがクラッシュしました:", err);
process.exit(1);
});
本番環境で得た苦労の教訓
複数の高トラフィックプロジェクトにTemporalをデプロイしてきた経験から、デバッグの時間を何時間も節約できる4つのヒントを紹介します。
1. バージョニングを尊重する
ワークフローは長命です。30日間のプロセスが実行中にworkflows.tsのコードを変更すると、履歴がコードと一致しなくなり「リプレイ」が失敗します。破壊的なロジック変更を安全に導入するには、必ずpatch APIを使用してください。
2. 冪等性はあなたの最大の味方
タスクが完了した瞬間にワーカーが落ちた場合、Activityが複数回実行される可能性があります。同じ操作で顧客に二重請求しないよう、StripeやデータベースへのリクエストにはIDempotencyキーを必ず渡してください。Redisを活用した冪等性の実装パターンも、この問題への対策として非常に有効です。
3. Web UIを活用する
TemporalのWeb UIはゲームチェンジャーです。すべてのイベントの視覚的なタイムラインを提供します。ワークフローがスタックしたとき、ログを掘り返すだけでなく、UIでどのActivityがタイムアウトしているかを確認し、失敗の完全なスタックトレースをレビューしましょう。
4. インタラクティブ性にはSignalを使う
ワークフローは「起動したら放置」ではありません。月の途中でユーザーが「サブスクリプションをキャンセル」をクリックするような外部イベントを処理するには、Signalを使いましょう。内部状態を失わずに、ワークフローが現実世界の出来事に反応できるようになります。
Temporalで構築するということは、「このエラーをどうキャッチするか?」という発想から「このプロセスはどう進化すべきか?」という発想への転換を意味します。状態管理の重い作業をオフロードすることで、実際にビジネスを動かすロジックに集中できるようになります。

