UPSIDER Tech Blog

TemporalによるDurableなワークフロー開発

こんにちは、UPSIDERで請求関連の機能の開発を担当しているチームです。

本記事では、Temporal というフレームワークを活用した Durableなワークフロー開発についてご紹介します。

私たちのチームでは、UPSIDERのさまざまなサービスにまたがる請求関連をスムーズかつ確実に処理するために、日々たくさんの課題に向き合っています。今回は、その中で出会った難しさと、それをどうTemporalを利用して乗り越えたかを技術的な観点からお届けします。

1. 「Durableな請求処理」が求められる背景

長期戦の請求処理との戦い

UPSIDERでは、月初の請求確定、請求日前日のリマインド通知、入金確認、そして万が一の引き落とし失敗時の督促対応など、1ヶ月以上に渡って連続的に発生する一連の請求イベントを管理しています。

単発の処理では終わらないーむしろ請求が確定したところから、長いマラソンが始まるのです。

これらを正確に、かつ確実に行う必要があります。なぜなら、請求に関わる処理での失敗はユーザーの信頼を失いかねないからです。

こうしたプロセスは、元々バッチ処理KubernetesのCronJobによって実装・運用されていました。が、ここには幾つかの問題が...。

この情報のサイロ化はまさに「魔女の鍋」。必要な情報をすべてのリソースからすくいあげて、ようやく全体像が見えてくる、そんな世界でした。

課題は「見えにくさ」と「わかりにくさ」

  • そもそも、どのバッチがいつ何をしているのかが見えない
  • 関連するAPI呼び出しの流れをたどるには、いくつものドキュメントを行き来する必要がある
  • 新しく入ってきた開発者にとっては、「えっ、請求周りが複雑すぎて無理😇」という洗礼

この状況を打破すべく私たち請求チームはもっと一貫性と透明性を持った請求処理基盤を作ろうと動き出しました。

2. バッチ処理に頼らない新たなアプローチ

“イベント×日時”の壁

請求処理は、スケジュールに基づいた処理実行が必要不可欠です。
となると、まず候補に上がるのが「バッチ処理」。ですが、先ほど紹介したような課題(見えない、つらい、カオス)を踏まえると、「もうちょっと違う方法をとりたいよね」というのがチームの共通認識でした。

—— じゃあどうする?

バッチ処理でスケジューリングしないといけない根本原因は、イベントAの後にイベントBを特定の日時まで待って実行することが難しいためです。。イベントの後に特定のイベントを実行する事自体は難しくありませんが、特定の日にならないと実行されないという機構を入れるにはそれなりに考えることがあります。

単純な例でいくと次のイベントを受け取ったらその時間までsleepをするなどが考えられますが以下のような問題があります。

  • sleep中にサーバーが落ちたらどうするの?
  • podやnodeのスケールアップ、ダウンでプロセスが強制終了されたら?
  • そもそも、1ヶ月sleepするプロセスって何者?笑

そんな疑問が次々に湧いてきて、「これは堅牢性もスケーラビリティも厳しいな...」という結論に。

あらわれた救世主「Temporal」

そこで私たちが選んだのが、Temporalというフレームワークです。
Temporalは、Durable Executionを実現するためのワークフローエンジンで、時間を跨いで実行される長期プロセスを安全に管理することができます。

—— Durable Executionとは?
実行状態や進行状況を失わずに保持できる仕組みのこと。

3. Durable Execution(耐久実行)とは?

イベントドリブンな世界の”つらみ”とその先にあるもの

さて、ここで少しだけ前提のお話しです。

請求のような長期間にわたるプロセスを、マイクロサービス環境で構築しようとすると、よく検討されるのが「イベント駆動アーキテクチャEDA」というアプローチです。

この構成自体はモダンかつ柔軟で、イベントを使って各サービス間を疎結合に保てるメリットがあります。
…が、イベントを”中心”にしてプロセス全体を組み立てようとすると、システムが複雑化してしまうことがあります

たとえば:

  • 処理の流れが複数のイベントに分かれてしまい、ビジネスロジックがいくつものサービスやキューに散在
  • 各サービスが独立してイベントを処理するため、「いま全体のプロセスはどこ?」がわかりにくい
  • 状態の管理やリトライ処理、補償トランザクションなどを自前で組み込む必要があり、ロジック以外の構築などに焦点を当てる必要が出てくる

これらは偶発的複雑性(Incidental Complexity)と呼ばれる種類の課題です。本質的ではない部分に時間を取られ、結果として機能追加も運用も大変になっていきます。

ワークフロー中心、という考え方

こうした課題に対して、Temporalがもたらしてくれたのは、ビジネスロジックを”プロセス(ワークフロー)”単位で捉えるという視点でした。

言い換えれば、分散されたイベントの連鎖ではなく、一つのまとまった物語としての処理を組み立てるイメージです。

この発想の転換こそが、Temporal導入の大きな転機でした。

4. Temporalが実現するDurable Executionの中身

「落ちても大丈夫」を支える仕組み

Temporalの凄さは「ずっと動き続けてくれるように見える」ということに尽きます。 たとえば、「3日後にリマインドメールを送って、さらにそこから20日後に入金確認する」みたいな流れを書いたときに、

  • その23日間ずっとプロセスを保持してくれる
  • サーバーが落ちたり、Podが再起動しても途中から再開できる
  • それらすべてが慣れ親しんだ言語でコード管理でき、可視化もできる

まるで「何年も落ちずに動き続ける理想的な単一サーバー」がいるかのようです。物理的にはそんなサーバーは存在しないかもしれませんが、Temporalはそれを履歴のリプレイによって実現してくれています。

リプレイの力: 失われない状態、そして制約

この「履歴のリプレイ」こそがTemporalの肝。
Temporalのワークフローは、「実行履歴(イベントログ)」を全て永続化しています。サーバーが落ちたとしても、過去のイベント履歴を使って状態を再構築することで、再起動後何事もなかったように動き続けるのです。
ただし、これには一つ重要な制約があります。

—— Workflowの中では、非決定的な処理を書いてはいけない

非決定的とは、リプレイのたびに結果が変わってしまう処理のこと。たとえば:

  • 乱数生成
  • 現在時刻の取得

このような処理はリプレイのたびに違う結果になってしまい、整合性が取れなくなります。
そのため、Temporalではこういった処理を「Activity」という別コンポーネントに切り出す、というルールがあるのです。

WorkflowとActivityの役割分担

Temporalでは処理を「Workflow」「Activity」に分けて設計します。

Workflow

  • Activityをどの順番で、どんな条件で実行するかを制御
  • タイムアウトやリトライの制御、条件分岐なども含む
  • 状態は全てリプレイ可能なように管理
  • スケジューリングや特定のsignalを待つなども簡単に実装可能

Activity

  • 「DBアクセス」「API呼び出し」など、副作用を持つ処理を担当
  • 非決定的な処理は基本全てこちらにまとめる
  • 実行中にPodが落ちても、実行のスナップショットが記録されていて、復旧後に途中から再開できる

5. Temporalの採用の背景: なぜ選んだのか?

請求処理のようなプロセスには、「数日待ってから実行」「障害が起きても復旧」「何かをトリガーに次の処理を始める」といった要件がついて周ります。
この複雑さに対応するために、他のワークフローツールも調査しましたが、私たちがTemporalを選んだ理由は以下の通りです:

  • Durable Executionによる高い信頼性
  • GoやJavaのような慣れ親しんだ言語でワークフローを記述できること
  • OSSかつKubernetes対応で、柔軟なデプロイも可能なこと
  • 長期間のワークフローにも対応できること

それぞれの選定ポイントをまとめた比較表はこちらです

6. Temporal導入による請求処理の変化

私たちのチームでは、これまでバッチやCronで分散していた請求処理を、1つのワークフローとしてTemporal上に集約しました。
たとえば、以下のようにWorkflowとActivityを実装しています。

Golangによるコードサンプル(Workflow)

func SimpleInvoiceWorkflow(ctx workflow.Context, invoiceID string) error {
    // Activity実行用オプション
    ao := workflow.ActivityOptions{
        StartToCloseTimeout: time.Minute,
        RetryPolicy: &temporal.RetryPolicy{
            InitialInterval:    time.Second * 5,
            BackoffCoefficient: 2,
            MaximumAttempts:    3,
        },
    }
    ctx = workflow.WithActivityOptions(ctx, ao)

    // 1) 請求メールを送信するアクティビティ
    if err := workflow.ExecuteActivity(ctx, "SendInvoiceEmailActivity", invoiceID).Get(ctx, nil); err != nil {
        return err
    }

    // 2) 翌日まで待機
    workflow.Sleep(ctx, 24*time.Hour)

    // 3) 支払いリマインドメールを送信するアクティビティ
    if err := workflow.ExecuteActivity(ctx, "SendReminderEmailActivity", invoiceID).Get(ctx, nil); err != nil {
        return err
    }

    return nil
}

Workflowでは一連の流れをコードでそのまま書けるようになりました。

workflow.Sleep(ctx, 24*time.Hour)

このコード一行で、KubernetesのCronJobや別バッチを用意せずに「1日待つ」という処理が可能になります。

  • スケジューリング専用の仕組みは不要
  • バッチ連携を頭で組み立てる必要はない

「このフローの状態っていまどこ?」という問いは、Workflowのどこまで進んでいるかは常にTemporal Cloudが状態を保持しているので簡単にわかります。

Golangによるコードサンプル(Activity)

アクティビティでは、実際に外部APIを呼び出したり、DBにアクセスしたりといった処理を行います。

func SendInvoiceEmailActivity(ctx context.Context, invoiceID string) error {
    subject := "【ご請求】請求書 " + invoiceID
    body := "請求書 " + invoiceID + " をお送りします。ご確認ください。"

    // 3) 実際のメール送信(ここではログ出力)
    log.Printf("To: customer@example.com\nSubject: %s\n\n%s\n", subject, body)
    return nil
}
  • 非決定的な処理(現在時刻の取得、外部API呼び出しなど)は全部アクティビティに押し込める。
  • アクティビティが失敗してもTemporalがリトライの仕組みを持っているため、自前でリトライのためのキューを用意する必要はなし

7. Durable Executionによって得られた本質的なメリット

Temporalの導入は、単に「バッチをやめた」という話ではありません。

私たちが本当に実感しているのは、ワークフロー中心の思考へのパラダイムシフトです。

ワークフローが”プロセスそのもの”になった

従来のバッチ処理では、「前のバッチがこれを処理して...次のバッチがあれをして...」というように、処理同士のつながりを頭の中で構築する必要がありました。
でもTemporalでは、ワークフローが状態とロジックを両方持ち続けてくれるため、
「〜日に何をやるか」だけを純粋に書けばよく、前後関係やリカバリまで含めてワークフローに”お任せ”できます。

リソースが一つにまとまる安心感

以前は:

という情報の分散っぷりが悩みの種でした。
今は:

設計が自然とシンプルになる

Temporalは「ワークフローで捉える」という設計思想をベースにしているため、バッチ的な複雑性を自然と避ける構造になっています。

  • 結果として、コードが読みやすく
  • 状態を把握しやすく
  • 障害にも強くなった

これは、「Durable Execution」の威力を身をもって体験している証拠です。

8. まとめ: ワークフローが世界をシンプルにする

本記事では、UPSIDERの請求チームにおける実例を通して、TemporalによるDurableなワークフロー開発をご紹介しました。

  • バッチ前提の設計では、処理が分散し、全体の把握や障害対応が困難に
  • Temporalによって、ワークフロー中心の設計へシフト
  • 長期間にわたる請求処理を、1つのリソースに集約
  • サーバーダウンやPodの再起動を気にせず、耐障害性のある状態管理が可能に

Temporalは魔法ではありませんが、「まるでサーバーが落ちないかのように動く世界」を実現してくれます。

次回: Temporalの「見える化」もすごい

今回のブログでは触れていませんが、Temporal CloudのUIで実際のイベント履歴を可視化できるのも導入後に気づいたポイントです。
「どこまで処理されたか」「どこで止まっているか」が一目でわかる便利さは、ぜひ次回のブログで詳しくお伝えしたいと思います!

以上、UPSIDER請求チームからのレポートでした!
最後まで読んでいただき、ありがとうございました🙌

We Are Hiring !!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。 herp.careers

カジュアル面談はこちら! herp.careers

Culture Deckはこちら📣 speakerdeck.com