はじめに
本記事では 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内実装はコスト重視の局所解として認められる。


コメント