はじめに:「Durable Execution」という宿題
サーバーがクラッシュしたらどうなるでしょうか?従来のアーキテクチャでは答えは「場合による」です。タイムアウトしてリトライすれば重複処理が発生し、部分的な状態が壊れて次のステップがおかしくなります。特に保険金請求処理のように 数分から数日 かかるワークフローでは、中断はほぼ不可避です。
業界ではこの問題を解決するために、専用のオーケストレーションクラスターやクラウド管理型ワークフローサービスが使われてきました。しかし、これらのソリューションには 運用の複雑さ、インフラ依存、アーキテクチャ上の制約 という代償が伴います。
Airbnbはこの問題に正面から取り組みました。外部依存なしに、サービス内部に直接ワークフローエンジンを埋め込むこと。そうして生まれたのが Skipper です。
本記事はAirbnbエンジニアリングブログの公開記事をベースに、日本の開発者視点でコアインサイトと実践的な適用ポイントを再構成しました。(根拠資料:原文リンク)

既存ソリューションの限界:外部オーケストレーションとドメインロジックの断絶
外部オーケストレーションの問題点
専用オーケストレーションエンジンは「Exactly-Onceセマンティクス」を保証し、実戦で検証された信頼性を提供します。しかし 専用インフラ(クラスター+永続化レイヤー) が必要であり、それはすなわち 運用の専門知識 を要求します。
Airbnbの「Tier 0」サービス(ユーザートランザクションに直接影響する最重要サービス)にとって、オーケストレーションクラスターの障害は全依存サービスのワークフローを停止させる 単一障害点(SPOF) となります。クラウド管理型サービスは運用負荷を軽減しますが、ベンダーロックイン、規制/データ処理問題、実行時間制限 という別のリスクを抱えます。
ドメインロジックの断片化
より微妙な問題は ドメインロジックが断片化 することです。キューコンシューマー、スケジュールジョブ、コールバックエンドポイント、整合性スクリプトにビジネスプロセスが散らばります。コード上で「このビジネスプロセスが実際に何をしているか」一目で把握できる場所がありません。さらに、各断片はリトライバックオフ、重複排除チェック、タイムアウト処理といったインフラ関心事と絡み合います。
日本のSI/スタートアップ環境でも似た経験をされた方も多いのではないでしょうか。 複数のマイクロサービスにまたがる補償トランザクションを実装していると、「なぜこのロジックがここに?」という瞬間が必ずあります。

Skipperの設計哲学:「組み込み型」という選択
Skipperは サービス内にライブラリとして埋め込まれる(Embedded) ワークフローエンジンです。5つの原則が設計を導きました。
| 原則 | 説明 |
|---|---|
| 簡潔なユーザビリティ (Succinct Ergonomics) | ワークフローコードがビジネスロジックそのもののように読めること |
| 単一障害点の排除 | 各サービスが独立してワークフロー処理、中央コーディネーターなし |
| 既存インフラの活用 | サービスが既に使用しているMySQLなど同一DBを使用 |
| セルフサービス統合 | ライブラリ依存関係の追加のみで使用可能 |
| パフォーマンス中立性 (Performance-Neutral) | ワークフローエンジンがホストサービスのスケーラビリティを制約しない |
実際のコードで見るSkipper
// 1) 通常の型付き呼び出しのように実行(コード生成クライアント不要)
val out = workflow("reservation:${req.id}").execute(req)
// 2) ワークフローロジックは普通のKotlinクラス
class ChargeAndAccept : Workflow() {
private val billing = actions()
private val reservations = actions()
@StateParam var paymentCaptured = false
@WorkflowMethod
suspend fun execute(r0: Reservation): Reservation {
val r1 = billing.charge(r0) // 耐久性のあるサイドエフェクト境界
waitUntil { paymentCaptured } // 耐久性のある待機(再起動後に再開)
return reservations.markAccepted(r1)
}
}
// 3) サイドエフェクトはActionsに;1つのアノテーションでチェックポイント可能
class BillingActions : Actions() {
@Execute(checkpoint = true)
suspend fun charge(r: Reservation): Reservation =
billingApi.chargeAsync(r.id, r.amount).await()
}
このコードは自然に読めます:「支払い、キャプチャされるまで待機、予約を承認する」。リトライロジック、キュー管理、非同期調整コードが一切見えません。
耐久性の核心:リプレイ(Replay)
Skipperは チェックポイントされたアクション結果 をDBに保存し、障害発生時にワークフローメソッドを最初から リプレイ します。既に実行されたアクションは再実行されず、チェックポイントされた結果を即座に返します。waitUntil のような待機区間では現在の状態を保存し、ワークフローは 休止(Hibernate) 状態に入り、コンピューティングリソースを全く消費しません。
ハッピーパス(Happy Path)ではオーバーヘッドがほぼありません。 ほとんどのワークフローエンジンが全ての実行にオーバーヘッドを課すのに対し、Skipperはクラッシュが発生した時のみリプレイメカニズムが動作します。これは レイテンシに敏感で高スループットが必要なサービス に非常に適した設計です。
補償トランザクション:@Compensate
Skipperは補償(Compensation)を 第一級市民(First-class citizen) として扱います。@Compensate アノテーションで各アクションの「実行取り消し」メソッドを定義すれば、アクション失敗時にSkipperが自動的に逆順で補償メソッドを実行します。分散トランザクションなしで 結果的整合性(Eventual Consistency) を達成するわけです。
class BillingActions : Actions() {
@Execute(checkpoint = true)
suspend fun charge(r: Reservation): Reservation { ... }
@Compensate
suspend fun compensateCharge(r: Reservation) {
// 支払いキャンセルロジック
refundApi.refund(r.id, r.amount)
}
}

実践適用と限界:日本企業が注目すべきポイント
生産性と拡張性
Skipperは1年以上プロダクションで運用され、保険、決済、メディア、インフラなど15以上のユースケース をサポートしています。ピーク時にはAmazon DynamoDB上で 毎秒10,000ワークフロー を処理する拡張性が実証されています。
注意点と限界
- 決定論(Determinism)要件: リプレイモデルではワークフローメソッドが決定論的である必要があります。API呼び出し、時間依存ロジック、乱数生成などは全てActions内に配置しなければなりません。
- 少なくとも1回実行(At-least-once): アクション実行後、チェックポイント前にクラッシュすると重複実行される可能性があります。アクションは冪等(Idempotent)である必要があります。
- ワークフロー進化(Evolution): 実行中のワークフロー構造を変更すると問題が発生します。バージョニング戦略が必須です。
日本企業の環境では、特に レガシーシステムとの統合 を考慮する必要があります。SkipperはJava/Kotlinに最適化されているため、Node.jsやPythonベースのマイクロサービスが多い環境では適用が難しい場合があります。
次のステップ学習方向
- Temporal.io / Uber Cadence: 外部オーケストレーションエンジンの代表格。Skipperとの比較を通じて「組み込み型 vs 外部型」のトレードオフを理解しましょう。
- AWS Step Functions / Azure Durable Functions: クラウド管理型サービスのメリット・デメリットを把握すると、Skipperの設計判断がより明確に見えてきます。
- Sagaパターン: 補償トランザクションの概念をより深く学びたい場合は、分散トランザクションにおけるSagaパターンを調べてみてください。
Skipperの核心的な洞察は 「リプレイベースの実行 + チェックポイントアクション」 によって、調整サービスなしで耐久性を確保できるという点です。あらゆる状況に完璧な解決策ではありませんが、依存関係を最小限にしながら耐久性のある実行が必要なサービス であれば、十分に検討する価値があります。
合わせて読みたい記事