長時間実行タスクにおけるイベント駆動ワークフローとリクエスト-レスポンスAPIの比較
承認、タイマー、再試行、監査トレイルを中心に、長時間実行プロセスに対してイベント駆動ワークフローとリクエスト–レスポンスAPIを比較します。

ビジネスアプリで長時間処理が厄介な理由
「長時間実行」のプロセスは一回の短いステップで終わらないときのことを指します。人の対応や時間、外部システムに依存するため、数分、数時間、あるいは数日かかることがあります。承認や引き継ぎ、待機を伴うものはすべてこの分類に入ります。
ここで単純なリクエスト–レスポンスAPIの考え方が崩れ始めます。API呼び出しは短いやり取りを想定しています: リクエストを送って応答を受け取り、次に進む。長時間の作業は章立てされた物語のようなものです。途中で止めて、正確にどこにいるかを記憶し、後で推測せずに続行する必要があります。
これは日常のビジネスアプリでもよく見られます: マネージャーや経理の承認が必要な購入申請、書類確認を待つ社員のオンボーディング、決済プロバイダに依存する返金、レビュー後に適用されるアクセス要求など。
チームが長時間プロセスを単一のAPIコールのように扱うと、予測可能な問題がいくつか出てきます:
- サーバー再起動やデプロイでアプリが状態を失い、再開できない。
- 再試行が重複を生む: 二重の支払い、二重メール、二重承認。
- 所有権が曖昧になる: 次に誰が動くべきか(依頼者、マネージャー、システムジョブ)が不明。
- サポートに可視性がなく、「どこで止まっているのか?」に答えられない。
- タイマーやリマインドなどの待機ロジックが壊れやすいバックグラウンドスクリプトになる。
具体例: 社員がソフトウェアのアクセスをリクエストしたとします。マネージャーはすぐ承認するが、ITのプロビジョニングに2日かかる。アプリがプロセス状態を保持してリマインドを送り、安全に再開できなければ、手動フォローやユーザーの混乱、追加作業が発生します。
だからこそ、長時間のビジネスプロセスに対してイベント駆動ワークフローとリクエスト–レスポンスAPIの選択が重要になります。
2つのメンタルモデル: 同期呼び出し vs 時間にまたがるイベント
最も単純な比較は次の問いに集約されます: 作業はユーザーが待っている間に終わるのか、それともユーザーが離れた後も続くのか?
リクエスト–レスポンスAPIは単一のやり取りです: 1回の呼び出しで応答が返る。レコード作成、見積もり計算、在庫確認のように短時間で予測可能な作業に向いています。サーバーが処理を行い、成功か失敗を返してやりとりは終わります。
イベント駆動ワークフローは時間をかけた一連の反応です。何かが起きる(注文作成、マネージャーが承認、タイマーが期限になる)とワークフローが次のステップに進みます。このモデルは、引き継ぎ、待機、再試行、リマインドを含む作業に適しています。
実用的な違いは「状態」です。
リクエスト–レスポンスでは状態は現在のリクエストとサーバーメモリにあることが多く、応答が返されると消えます。イベント駆動ワークフローでは、後で再開できるように状態を保存する必要があります(例: PostgreSQLなど)。
障害処理も変わります。リクエスト–レスポンスは通常エラーを返してクライアントに再試行を促します。ワークフローは失敗を記録し、状況が改善したら安全に再試行できます。また各ステップをイベントとしてログに残せば履歴の再構築が容易です。
単純な例: 「経費報告を送信」は同期でよい場合がありますが、「承認を得て3日待ち、マネージャーにリマインドしてから支払い」は同期では扱えません。
承認: 各アプローチが人の判断をどう扱うか
承認は長時間処理が現実になる場面です。システムのステップはミリ秒で終わるが、人は数分〜数日で応答するかもしれません。設計上の重要な選択は、その待ちを「一時停止したプロセス」としてモデル化するか、「後で届く新しいメッセージ」として扱うかです。
リクエスト–レスポンスAPIでは、承認はしばしば扱いにくい形になります:
- ブロッキング(現実的でない)
- ポーリング(クライアントが「承認された?」と何度も聞く)
- コールバック/ウェブフック(サーバーがあとで呼び出す)
どれも機能しますが、人の時間とAPI時間を橋渡しするための配線が必要になります。
イベントでは承認は物語の一部のように見えます。アプリが「ExpenseSubmitted」を記録し、後で「ExpenseApproved」か「ExpenseRejected」を受け取ります。ワークフローエンジン(または独自のステートマシン)は次のイベントが届いたときだけレコードを進めます。これは多くの人がビジネス手順を考えるやり方に合致します: 提出、審査、決定。
複数承認者やエスカレーションルールがあると複雑さはすぐに現れます。マネージャーと経理の両方の承認が必要だがシニアマネージャーが上書きできる、などのルールを明確にモデル化しないと、プロセスは推測しにくく、監査も難しくなります。
拡張可能な単純な承認モデル
実用的なパターンは、1つの「リクエスト」レコードを保持し、意思決定を別に保存することです。こうすることで多くの承認者をサポートしてもコアロジックを書き換える必要がありません。
ファーストクラスのレコードとしていくつかのデータをキャプチャします:
- 承認リクエスト自身: 承認対象と現在のステータス
- 個別の決定: 誰が決定したか、承認/却下、タイムスタンプ、理由
- 必要な承認者: 役割や個人、順序ルール
- 結果ルール: 「いずれか一人」、「過半数」、「全員必須」、「上書き可」
どの実装にしても、誰がいつ何を承認したか、なぜしたかをログ行ではなくデータとして必ず保存してください。
タイマーと待機: リマインダー、期限、エスカレーション
待機は長時間タスクを厄介にする部分です。人は昼休みに行き、カレンダーは埋まり、「折り返します」は「誰が担当?」になります。ここがイベント駆動ワークフローとリクエスト–レスポンスAPIの最も明確な違いのひとつです。
リクエスト–レスポンスでは時間は扱いにくいです。HTTPはタイムアウトがあるため、2日間接続を開いたままにはできません。チームは通常ポーリングやデータベースを走査するスケジュールジョブ、あるいは期限切れを扱う手動スクリプトに頼ることになります。これらは機能しますが、待機ロジックがプロセスの外側にあり、ジョブが二度実行された場合やレコードが直前に変わったケースを見落としやすいです。
ワークフローは時間を通常のステップとして扱います。たとえば「24時間待つ、リマインドを送る、合計48時間でエスカレーションする」といった記述ができます。システムが状態を保持するので、期限がcronプロジェクトのどこかに隠れることはありません。
単純な承認ルールの例:
提出後1日待つ。まだ“Pending”ならマネージャーに通知する。2日後でも保留ならマネージャーの上長に再割当てし、エスカレーションを記録する。
重要な点は、タイマーが発火したときに世界が変わっている場合にどうするかです。良いワークフローは必ず実行前に最新状態を再確認します:
- 最新のステータスを読み込む
- まだ保留か確認する
- 担当者が有効か確認する(チームの変更が起こる)
- 何を決めたか、なぜかを記録する
再試行と失敗からの回復(重複アクションを避ける)
再試行は、支払いゲートウェイのタイムアウト、メールプロバイダの一時エラー、アプリがステップAを保存してからクラッシュしてステップBが未完了になるなど、制御できない理由で何かが失敗したときに行うものです。危険は明白で、再試行で同じアクションが二度行われることです。
リクエスト–レスポンスでは、クライアントがエンドポイントを呼び、応答が明確な成功でない場合に再試行するのが典型です。安全にするにはサーバー側で繰り返し呼び出しを同じ意図として扱う必要があります。
実用的な対策は冪等性キーです: クライアントが pay:invoice-583:attempt-1 のような一意のトークンを送ります。サーバーはそのキーに対する結果を保存し、繰り返しには同じ結果を返します。これで重複請求や重複チケット、重複承認を防げます。
イベント駆動ワークフローでは別の重複リスクがあります。イベントはたいていat-least-onceで配信されるため、重複が発生します。消費者はイベントID(または invoice_id + step のようなビジネスキー)を記録し、繰り返しを無視する必要があります。これはワークフローオーケストレーションのコアの違いです: リクエスト–レスポンスは呼び出しの安全なリプレイに焦点を当て、イベントはメッセージの安全なリプレイに焦点を当てます。
どちらのモデルでもうまくいく再試行ルールがいくつかあります:
- バックオフを使う(例: 10秒、30秒、2分)
- 最大試行回数を設定する
- 一時的エラー(再試行)と恒久的エラー(即失敗)を分ける
- 繰り返し失敗は「要対応」状態にルーティングする
- すべての試行をログに残して後で説明できるようにする
再試行はプロセスの中で明示的に扱うべきで、隠れた振る舞いにしないこと。そうすれば障害は可視化され、修正しやすくなります。
監査トレイル: プロセスを説明可能にする
監査トレイルは「なぜ」の記録です。「なぜこの経費は却下されたのか?」と聞かれたときに、数ヶ月後でも推測せずに答えられる必要があります。これはイベント駆動ワークフローとリクエスト–レスポンスAPIの両方で重要ですが、取り組み方は異なります。
長時間プロセスでは、物語を再生できる事実を記録します:
- 実行者: 誰がやったか(ユーザー、サービス、システムタイマー)
- 時刻: いつか(タイムゾーン付き)
- 入力: 当時何が分かっていたか(金額、ベンダー、ポリシー閾値、承認)
- 出力: どんな決定やアクションがあったか(承認、却下、支払、再試行)
- ルールバージョン: どのポリシー/ロジックバージョンを使ったか
イベント駆動ワークフローは各ステップが「ManagerApproved」や「PaymentFailed」などのイベントを自然に生成するため、監査がしやすくなります。重要なのはイベントを十分に記述的にし、ケースごとに問い合わせ可能な場所に保存することです。
リクエスト–レスポンスAPIでも監査は可能ですが、物語がサービス間で散らばりがちです。1つのエンドポイントは「approved」をログし、別は「payment requested」、さらに別は「retry succeeded」をログする。形式やフィールドが異なると監査は探偵仕事になります。
簡単な対策は共通の「ケースID」(相関ID)を使うことです。プロセスインスタンスに一つの識別子を付け(例: "EXP-2026-00173")、すべてのリクエスト、イベント、DBレコードに付ければ、全行程を追跡できます。
正しいアプローチの選び方: 強みとトレードオフ
どちらを選ぶかは、今すぐ答えが必要か、それとも数時間〜数日かけてプロセスが進むかに依存します。
リクエスト–レスポンスは作業が短くルールが単純な場合に向いています。ユーザーがフォームを送信し、サーバーが検証してデータを保存し、成功またはエラーを返すようなケースです。作成、更新、権限チェックのような単一ステップの操作には適しています。
問題になるのは「単一リクエスト」が実は多段階になってしまうときです: 承認待ち、複数外部システムへの呼び出し、タイムアウト処理、次に何が起きるかで分岐する。接続を開き続けると脆弱ですし、待機と再試行をバックグラウンドジョブに押し込むと理論的に理解しにくくなります。
イベント駆動ワークフローは時間をまたぐ物語に強みがあります。各ステップが新しいイベント(承認、拒否、タイマー発火、支払い失敗)に反応して次を決めることで、一時停止・再開・再試行・理由の追跡が容易になります。
トレードオフは現実的です:
- シンプルさ vs 耐久性: リクエスト–レスポンスは導入が簡単、イベント駆動は遅延に強い。
- デバッグの流儀: リクエスト–レスポンスは直線的、ワークフローはステップ横断での追跡が必要。
- ツールと習慣: イベントは良いロギング、相関ID、明確な状態モデルが必要。
- 変更管理: ワークフローは枝分かれや進化を扱いやすい設計にすると新しい経路に強い。
実用例: マネージャー承認→経理レビュー→支払いの経費報告。支払いが失敗した場合は重複支払いを避けつつ再試行したい。これはイベント駆動が自然です。単に「提出して簡単チェックをする」ならリクエスト–レスポンスで十分なことが多いです。
ステップバイステップ: 遅延に耐える長時間プロセスの設計
長時間プロセスが壊れるのは地味な原因がほとんどです: ブラウザタブが閉じる、サーバーが再起動する、承認が3日放置される、支払いプロバイダがタイムアウトする。どのモデルを選ぶにせよ、最初からこれらの遅延を想定して設計してください。
まず保存して再開できる最小限の状態を定義します。データベースで現在の状態を指せないなら、本当に再開可能なワークフローは作れていません。
単純な設計シーケンス
- 境界を定める: トリガー開始、終了条件、主要な状態(Pending approval、Approved、Rejected、Expired、Completed)を定義する。
- イベントと決定に名前を付ける: 時間経過で起こり得ること(Submitted、Approved、Rejected、TimerFired、RetryScheduled)を記述。イベント名は過去形にする。
- 待機ポイントを選ぶ: 人、外部システム、期限でプロセスが止まる箇所を明示する。
- ステップごとにタイマーと再試行ルールを追加する: 時間経過や呼び出し失敗時の挙動(バックオフ、最大試行、エスカレーション、諦め)を決める。
- プロセスの再開方法を定義する: 各イベントやコールバックで保存した状態を読み込み、まだ有効か検証してから次の状態へ移す。
再起動に耐えるには、続行に必要な最小データを永続化してください。推測なしにやり直せるだけの情報を保存します:
- プロセスインスタンスIDと現在の状態
- 次に誰がアクションできるか(担当者/役割)とその決定
- 期限(due_at、remind_at)とエスカレーションレベル
- 再試行メタデータ(試行回数、最後のエラー、次の再試行時刻)
- 外部副作用用の冪等性キーや「既に実行済み」フラグ(メッセージ送信やカード課金など)
保存したデータから「今どこにいるか」と「次に何が許されるか」を再構築できれば、遅延はもはや怖くありません。
よくあるミスとその回避法
長時間プロセスは実ユーザーが来て初めて壊れることが多いです。承認が2日放置され、再試行が誤ったタイミングで発火し、二重支払いや監査痕跡の欠落が起きます。
よくあるミス:
- 人の承認を待ちながらHTTPリクエストを開いたままにする。タイムアウトし、サーバーリソースを占有し、ユーザーに誤った期待を与える。
- 冪等性を使わずに再試行する。ネットワーク障害が二重請求や二重メール、二重の「承認済み」を生む。
- プロセス状態を保存しない。状態がメモリにあると再起動で消える。ログだけだと安全に続行できない。
- ぼやけた監査トレイル。イベントが異なる時計で記録され、フォーマットがバラバラだとインシデントやコンプライアンス調査で信頼できない。
- 非同期と同期を混ぜて単一の真実を持たない。あるシステムは「Paid」と言い、別は「Pending」と言うような状態になる。
簡単な例: 経費がチャットで承認され、Webhookが遅れて到着し、支払いAPIが再試行される。状態保存と冪等性がなければ再試行で二重支払いが発生し、記録だけでは原因が分からなくなります。
ほとんどの修正は明示化にあります:
- 状態遷移(Requested、Approved、Rejected、Paid)をデータベースに永続化し、誰/何が変えたかを記録する。
- 支払い、メール、チケットなどの外部副作用には冪等性キーを使い、その結果を保存する。
- 「リクエストを受け付ける」処理と「作業を完了する」処理を分離する: まず速やかに受け付けを返し、ワークフローはバックグラウンドで完了させる。
- タイムスタンプはUTCに統一し、相関IDを付け、リクエストと結果の両方を記録する。
作る前の簡単チェックリスト
長時間の作業は完全な呼び出しよりも、遅延や人、障害の後でも正しく続行できることが重要です。
「安全に続けられる」とは何かを書き出してください。アプリが途中で再起動しても、最後の既知のステップから推測なしに再開できるべきです。
実用チェックリスト:
- クラッシュやデプロイ後にプロセスがどう再開するかを定義する。どの状態が保存され、次に何が動くか?
- すべてのインスタンスに一意のプロセスキーを与え(例: ExpenseRequest-10482)、明確なステータスモデルを持つ(Submitted、Waiting for Manager、Approved、Paid、Failed)。
- 承認は結果だけではなくレコードとして扱う: 誰がいつ承認/却下したか、理由やコメントを残す。
- 待機ルールをマップする: リマインド、期限、エスカレーション、失効。各タイマーの所有者(マネージャー、経理、システム)を明示する。
- 障害処理を計画する: 再試行は制限され安全であること、そして人がデータを修正したり再試行を承認できる「要レビュー」停止を用意する。
健全性テスト: 支払いプロバイダがタイムアウトしたときに既に課金済みだった場合でも、二重請求を防ぎつつプロセスを完了できる設計か想像してみてください。
例: 期限付き承認と支払い再試行を含む経費承認
シナリオ: 社員が$120のタクシー領収書を提出。48時間以内にマネージャー承認が必要。承認されればシステムが社員に支払いを行う。支払いが失敗したら安全に再試行し、明確に記録を残す。
リクエスト–レスポンスの流れ
リクエスト–レスポンスではアプリは往々にして継続的に状態を確認する会話のように振る舞います。
社員がSubmitを押すと、サーバーはステータス「Pending approval」で払い戻しレコードを作りIDを返します。マネージャーに通知が行くが、社員側は通常ポーリングで状態を取得します(例: 「IDで払い戻しステータスをGET」)。
48時間の期限を強制するには、期限切れリクエストを探すスケジュールジョブを走らせるか、期限タイムスタンプを保存してポーリング時にチェックする必要があります。ジョブが遅れると表示が古くなります。
マネージャーが承認するとサーバーはステータスを「Approved」にして支払いプロバイダを呼びます。Stripeが一時エラーを返した場合、サーバーは今再試行するか後で再試行するか、あるいは失敗扱いにするかを決めなければなりません。慎重に冪等性キーを用意していなければ、再試行で二重支払いが発生する恐れがあります。
イベント駆動の流れ
イベント駆動モデルでは各変更が事実として記録されます。
社員が提出すると "ExpenseSubmitted" イベントが発生します。ワークフローは開始して、48時間のタイマーイベント "DeadlineReached" か "ManagerApproved" を待ちます。タイマーが先に来たらワークフローは "AutoRejected" とその理由を記録します。
承認が来たらワークフローは "PayoutRequested" を記録して支払いを試みます。Stripeがタイムアウトしたら "PayoutFailed"(エラーコード付き)を記録し、再試行をスケジュール(例: 15分後)し、冪等性キーを使って一度だけ "PayoutSucceeded" を記録します。
ユーザーに見せる状態はシンプルです:
- 保留中(48時間残り)
- 承認済み、支払い手続き中
- 支払い再試行をスケジュール
- 支払い完了
監査トレイルはタイムラインとして読めます: 提出、承認、期限確認、支払い試行、失敗、再試行、支払い完了。
次のステップ: モデルを実働アプリにする
まずは1つの実際のプロセスをエンドツーエンドで作り、それから一般化してください。経費承認、オンボーディング、返金処理は良い出発点です。これらは人のステップ、待機、失敗経路を含むため学びが多いです。目標は小さく保つ: 1つのハッピーパスと最も一般的な2つの例外パス。
プロセスを画面ではなく状態とイベントとして書いてください。例: "Submitted" -> "ManagerApproved" -> "PaymentRequested" -> "Paid"。分岐として "ApprovalRejected" や "PaymentFailed" を用意します。待機ポイントと副作用が明確になれば、イベント駆動ワークフローとリクエスト–レスポンスAPIの選択は実用的になります。
プロセス状態をどこに置くかを決めます。フローが単純で更新を1箇所に強制できるならデータベースで十分です。タイマーや再試行、分岐が必要ならワークフローエンジンが何を次にすべきかを追跡してくれるので便利です。
最初から監査フィールドを追加してください。誰が何をいつやったか、なぜ(コメントや理由コード)を保存します。誰かに「なぜこの支払いが再試行されたのか?」と聞かれてもログを漁らずに明確に答えられるようにします。
こうしたワークフローをノーコードプラットフォームで構築する場合、AppMaster (appmaster.io) のような選択肢があります。PostgreSQLにデータをモデル化し、視覚的にプロセスロジックを作れるため、承認や監査トレイルをWeb・モバイルで一貫して扱いやすくなります。
よくある質問
ユーザーが待っている間に作業が短時間で確実に終わる場合(レコード作成やフォーム検証など)、request-responseを使います。プロセスが数分〜数日に及び、人の承認やタイマー、再試行、安全な再開が必要な場合はイベント駆動ワークフローが適しています。
長時間のタスクは単一のHTTPリクエストに収まらないためです。接続はタイムアウトし、サーバーは再起動し、作業は人や外部システムに依存します。単一の呼び出しとして扱うと、状態を失い、再試行で重複が発生し、待機処理が散在したバックグラウンドスクリプトになりがちです。
デフォルトの方法として、データベースに明確なプロセス状態を永続化し、明示的な遷移だけで進めると良いです。プロセスインスタンスID、現在のステータス、次に誰がアクションできるか、重要なタイムスタンプを保存しておけば、デプロイやクラッシュ、遅延後に安全に再開できます。
承認はブロッキングや頻繁なポーリングではなく、決定が届いたときに再開する“一時停止”ステップとしてモデル化するのが最も扱いやすいです。誰がいつ承認/却下したか、理由をデータとして記録してください。そうすればワークフローは予測可能に進み、後の監査も容易になります。
ポーリングは単純なケースでは機能しますが、クライアントが「終わったか?」を繰り返し尋ねるためノイズと遅延を生みます。デフォルトでは状態変更時に通知をプッシュし、クライアントは必要に応じて更新する方が良いです。サーバー側が単一の真実(source of truth)であるべきです。
時間をプロセスの一部として扱い、期限やリマインド日時を保存しておき、タイマーが発火したときには最新状態を再確認してから行動します。これにより、既に承認済みのものにリマインドが送られるなどの誤動作を避けられます。ジョブが遅延・二重実行しても一貫性を保てます。
支払いやメール送信などの副作用には必ず冪等性キーを使い、そのキーに対する結果を保存してください。そうすれば再試行しても同じ意図は同じ結果を返し、重複実行を防げます(例: pay:invoice-583:attempt-1)。
イベントは少なくとも一度配信されることを前提にし、消費側で重複排除する設計にします。実用的にはイベントIDやステップ用のビジネスキーを保存し、重複は無視することでリプレイによる二重実行を防ぎます。
監査トレイルには事実のタイムラインを残します: 実行者、タイムスタンプ、その時点での入力、結果、適用されたルールやポリシーのバージョンです。さらにそのプロセスに共通のケースID(相関ID)を付けておくと、サポートがどこで詰まっているかを容易に追跡できます(例: EXP-2026-00173)。
ケースとしてのリクエストレコードを1つ持ち、個別の決定は別テーブルで保存します。状態遷移は永続化された遷移を通じて進め、再生可能であることが重要です。AppMaster(appmaster.io)のようなノーコードツールでは、PostgreSQLにデータをモデル化し視覚的にロジックを組めるため、承認や再試行、監査フィールドの一貫性を保ちやすくなります。


