BFFがマイクロサービス間の制御を担うのはアリか?分散トランザクションを処理する場所のベストプラクティス

未分類

はじめに

本記事では BFF(Backend for Frontend)層に「複数マイクロサービスをまたぐ更新処理(=分散トランザクション)」を実装してよいのか? という実務上の悩みに答えます。

前提として、BFFはマイクロサービスアーキテクチャの必須構成要素ではありません。しかし UX向上・クライアント多様化(Web / Mobile / Admin)への対応策として、実務的に導入されるケースが非常に多い構成パターンです。

そのような構成において、

✅「ユーザー視点では1回の操作」
→ 👇
☑️「裏では複数マイクロサービスA/B/Cを順番に呼び出す必要がある」

というケースに直面したとき、どこにオーケストレーション(制御)を置くべきか? が設計の肝になります。


結論(先に要点だけ欲しい人向け)

設計パターン実装場所推奨度備考
専用オーケストレーター(State Machine / Workflow Engine)BFFとは独立したサービス or マネージド(Step Functions / Temporalなど)最も推奨Saga / TCC / 補償・監査・リトライに最適
⚠️ BFF内にオーケストレーション(Saga/TCC)を埋め込むTypeScript / Node内にtry→catchで実装小規模&暫定なら可冪等性・補償・監査を絶対に忘れてはいけない

なぜBFFがマイクロサービス統合の場になるのか?

[Client] → [BFF] → (UserService / BillingService / NotificationService ...)
  • BFFは 「UIから見た1回の操作を、裏側の複数マイクロサービスに変換する集約レイヤー」 として実務上期待される
  • しかし「集約」と「業務一貫性の制御(Saga/TCC)」は本来別の責務

アンチパターン:BFF内にSagaを書く例(TypeScript)

// ⚠️ アンチパターン(BFFが業務フローを抱えてしまっている例)

app.post("/updateUser", async (req, res) => {
  const body = req.body;

  try {
    const aRes = await axios.post("http://service-a/update", body);
    try {
      const bRes = await axios.post("http://service-b/update", body);
      return res.json({ status: "ok" });
    } catch (errB) {
      // 補償(ロールバック)
      await axios.post("http://service-a/revert", { id: body.id });
      return res.status(500).json({ error: "B update failed, rolled back A" });
    }
  } catch (errA) {
    return res.status(500).json({ error: "A update failed" });
  }
});

これがなぜ危険なのか?

  • UI改修のたびに業務フローが巻き込まれる(責務の混濁)
  • BFFが「長時間処理・リトライ・補償・監査」の責任を持つことになる
  • スパイク(F5連打)で補償APIが大量発火しやすい
  • 観測(成功/失敗のトレース)が困難

推奨パターン:オーケストレーターを外出しする

理想構成(AWS Step Functionsを例に)

sequenceDiagram
  Client ->> BFF: POST /updateUser
  BFF ->> Orchestrator: StartExecution(updateUser)
  Orchestrator ->> Service A: update()
  Orchestrator ->> Service B: update()
  Orchestrator -->> Service A: revert() (on failure)
  Orchestrator ->> BFF: Completed(SUCCEEDED/FAILED)
  BFF ->> Client: 202 + operationId

BFFの役割は「起動するだけ」

// ✅ BFFは Orchestrator に対して 起動コマンドを送るだけ

app.post("/updateUser", async (req, res) => {
  const { data } = await axios.post("https://orchestrator/start", {
    idempotencyKey: req.headers["Idempotency-Key"],
    payload: req.body,
  });

  return res.status(202).json({
    operationId: data.operationId,
    statusUrl: `/operations/${data.operationId}`,
  });
});

どうしてもBFF内でやりたくなる理由

現場で「オーケストレーターなんて今すぐ用意できない」と言われるのはよくあります。

✅ その場合 条件付きで暫定実装は「あり」 です。

暫定許容できる条件OKライン
処理時間数秒以内、待機・人手介入なし
補償API即時・冪等(何回叩いても同じ結果)
パス数2〜3ステップ以内(A→B程度)
リトライバックオフ制御済み / 再実行安全

暫定パターンで最低限守るべきガードレール

  • Idempotency-Key を全書き込みAPIに付ける
  • 補償API(revert / cancel)は必ず冪等に
  • UIとは必ず 202 + operationId で非同期応答にする
  • OpenTelemetry の Trace ID を BFF → A/B → 補償まで貫通させる

まとめ

選択肢メリットデメリット採用判断
❌ BFF内にSaga直書き速い/実装が簡単破綻しやすい/責務混濁暫定 & 小規模のみ
✅ 専用オーケストレータ外出し責務分離/監査・再実行可能初期コスト高長期的にはこちら一択

結論

BFFは「集約レイヤー」にはなり得ますが、「業務オーケストレーションの本丸」にはしてはいけません。
オーケストレーションは 専用サービス or ワークフローエンジンに外出しするのが原則
ただし「小さく・短く・完全冪等・すぐ外出し可能」なら 暫定BFF内実装はコスト重視の局所解として認められる

コメント

タイトルとURLをコピーしました