Offline Queueは、デバイスがオフラインのときにユーザー操作をローカルに保存し、接続が復元された後にサーバーに送信するメカニズムです。オフラインキューがないと、ユーザーはインターネットなしで行ったすべての操作を失い、モバイルアプリでは許容できません。Google Developers(2025年)によると、オフラインファーストアーキテクチャを導入すると、インターネットが不安定な地域でのユーザー維持率が30%向上します。
重要ポイント
Offline Queueは、デバイスがネットワークにアクセスできないときにアプリがローカルに保存する操作(作成、更新、削除)の順序付けられたコレクションです。接続が復元されると、キューはユーザーが実行した順序でサーバーに操作を送信します。
シナリオを想像してください:メッセンジャーユーザーがインターネットなしで地下鉄でメッセージを入力しています。「送信」をタップするたびに、Offline Queueに追加されます。電車がトンネルを出てネットワークが利用可能になると、すべてのメッセージが自動的に送信されます。ユーザーエクスペリエンス — シームレス:送信のわずかな遅延を除いて、オフラインだったことに気づきません。
Uber Engineering(2024年)によると、同社のオフラインキューは接続品質の低い地域で1日あたり200万以上の操作を処理しています。キューはFIFO順序とexactly-once保証付き配信メカニズムを備えたRoomローカルストレージを使用しています。
data class QueuedOperation(
val id: String,
val type: OperationType,
val endpoint: String,
val payload: String,
val timestamp: Long,
val retryCount: Int = 0,
val idempotencyKey: String
)
各操作には、再送信に必要なすべてのデータ(エンドポイント、リクエスト本文、タイムスタンプ、idempotencyKey)が含まれています。Roomデータベースは、アプリの再起動やOSのクラッシュ後もキューの永続性を保証します。
配信の保証 — キューの主な目的です。ユーザーは、ネットワークがその時点で利用できなくても、自分のアクション(メッセージ送信、いいね、注文)が完了することを確信できる必要があります。再試行メカニズムを備えたOffline Queueは、最終的な配信を保証します。
接続状態が悪い環境でのUX向上 — GSMA Mobile Economy Report(2025年)によると、世界中のモバイルユーザーの約40%が不安定なインターネット接続を利用しています。Offline Queueにより、地下鉄、エレベーター、遠隔地など、接続が断続的なあらゆる場所でアプリを使用できるようになります。
データ損失の削減 — キューがないと、オフラインで実行されたすべてのアクションが失われます。ユーザーが長いフォームに入力し、「送信」をタップしてネットワークエラーが表示されると、すべての入力が失われます。Offline Queueはデータを保存し、最初の機会に送信します。Google Docsの自動保存は、ドキュメントのオフラインキューの古典的な例です。
非同期同期 — キューにより、アプリは送信中にUIをブロックしません。ユーザーは作業を続け、同期マネージャーがバックグラウンドでキューを処理します。これはリアクティブアーキテクチャの原則に従い、インターフェースの応答性を向上させます。
キューの3つの層:ストレージ(永続化)、スケジューラー、エグゼキューター。ストレージ — QueuedOperationテーブルを持つRoom。スケジューラー — ネットワークが利用可能になったときに同期を開始するWorkManager(Android)またはBGTaskScheduler(iOS)。エグゼキューター — 操作を1つずつ送信する順次FIFOイテレーター。
処理順序 — データの整合性にとって重要です。ユーザーがレコードを作成してから編集した場合、両方の操作を同じ順序で送信する必要があります。そうしないと、サーバーは最初に存在しないレコードの更新を受け取ることになり、エラーになります。シーケンシャルFIFO — 操作間の依存関係を制御する厳密な順序付け。
マージ戦略 — キューにCREATEがあり、その直後に同じオブジェクトのDELETEがある場合、送信せずに両方の操作を削除できます。最終状態はオブジェクトが作成されていないことです。同様に、CREATE + UPDATEは最新データで1つのCREATEにマージできます。キューの最適化により、HTTPリクエスト数が削減され、同期が高速化されます。
Android Developers(2025年)によると、WorkManagerはAndroidでOffline Queueを処理する推奨方法です。デバイス再起動後も実行を保証し、ネットワーク制約をサポートし、NetworkType.CONNECTEDを介して再試行ポリシーを設定できます。
class SyncWorker(
private val context: Context,
private val params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = runCatching {
queueRepository.processNextBatch(batchSize = 10)
Result.success()
}.getOrDefault(Result.retry())
}
CoroutineWorkerは操作のバッチを処理し、失敗時にResult.retry()を返します。WorkManagerは指数バックオフで自動的に再試行します。これはAndroidで信頼性の高いOffline Queueを実現する最も簡単な方法です。
Exponential Backoff — 間隔を増やしながら再試行する標準的な戦略:2秒、4秒、8秒、16秒…最大しきい値まで。これにより、サーバーが一時的に利用できない場合の繰り返し過負荷を防ぎます。JavaライブラリResilience4j(2024年)は、設定可能なバックオフを備えた既製のRetry実装を提供しています。
最大試行回数 — 重要なパラメーターです。5〜10回試行しても操作が失敗する場合、それ以上の再試行は無駄で効果がありません。dead letter queueが推奨されます:試行を使い果たした後、操作は手動分析のために別のテーブルに移動されます。Microsoft Patterns & Practices(2024年)によると、dead letter queueは同期問題のデバッグを簡素化し、誤った操作がキューをブロックするのを防ぎます。
Jitter — ランダムなばらつき — バックオフ間隔にランダムな数を追加します。停止後に1000台のデバイスが同時にネットワークを回復した場合、すべてが同時に同期を開始します。Jitterにより時間的に分散され、サーバーでのCache Stampedeを防ぎます。完全なjitter:delay = random(0, backoff) — AWS(2024年)がAPIクライアントに推奨しています。
Last Write Wins(LWW) — 最も単純な戦略:競合が発生した場合、より新しいタイムスタンプの操作が優先されます。LWWには時刻同期が必要です — タイムスタンプはサーバーで生成されるか、論理クロック(ランポートクロック)を使用する必要があります。欠点:あるユーザーのデータが警告なしに別のユーザーのデータで上書きされる可能性があります。
OT(操作変換) — Google DocsやFigmaがオフラインモードを含むリアルタイム共同編集に使用するアルゴリズムです。OTは操作を変換してドキュメントの任意の状態に適用できるようにし、ロックなしで整合性を保証します。CRDT(競合回避複製データ型) — モバイルアプリで人気を集めているOTの代替手段:データは中央サーバーなしで数学的に競合を解決できるように構造化されています。
カスタムマージ — 単純なデータモデル(メモ、連絡先)のアプリでは、カスタムマージルールを実装できます。例えばメモの場合:テキストが2つのバージョンで変更された場合、区切り文字で連結してマージします。ユーザー解決の競合 — 自動マージが不可能な場合、ユーザーに両方のバージョンを表示して選択してもらいます。Dropbox(2024年)はオフラインファイルの競合にこのアプローチを使用し、「Conflicted Copy」プレフィックス付きのコピーを作成します。
Idempotency Key — サーバーが重複リクエストを検出するために使用する一意の操作識別子です。クライアントが同じキーで同じリクエストを送信した場合、サーバーは再実行せずに既に完了した操作の結果を返します。これは、ネットワークエラーによる再送信が発生する可能性があるOffline Queueにとって非常に重要です。
Idempotency keyの形式は、UUIDまたはリクエストパラメータのハッシュです。サーバーは重複を検出するために、一定期間(通常24時間)、完了したキーを結果とともに保存する必要があります。Stripe API(2024年)が参考例です:キーはIdempotency-Keyヘッダーで渡され、同じキーでの繰り返しリクエストはキャッシュされたレスポンスを返します。
クライアント側での生成 — キーは操作を送信する前にクライアントで作成され、QueuedOperationテーブルに保存されます。再試行時もキーは変更されません。Exactly-onceアーキテクチャ — クライアント側のidempotency keyとサーバー側の重複排除の組み合わせが、操作が2回実行されないことを保証する唯一の方法です。
fun createOperation(type: OperationType, payload: String): QueuedOperation =
QueuedOperation(
id = UUID.randomUUID().toString(),
type = type,
endpoint = type.endpoint,
payload = payload,
timestamp = currentTimeMillis(),
idempotencyKey = UUID.randomUUID().toString()
)
各操作は2つのUUIDを取得します:1つはキュー内のレコード識別子、もう1つはサーバー用のidempotency keyです。idempotencyKeyによるサーバー側の重複排除により、再送信時でも注文が重複しないことが保証されます。
よくある質問
キャッシュはオフラインで高速に読み取るためのデータコピーを保存します。Offline Queueはサーバーへの後続の書き込みのためにユーザー操作を保存します。キャッシュは読み取り用、キューは書き込み用です。両方のコンポーネントはオフラインファーストアーキテクチャで共存できます。
推奨上限 — 100〜500操作。それ以上はメモリオーバーフローとネットワーク復旧時の長時間の同期のリスクがあります。上限を超えた場合、アプリはユーザーに警告し、操作の優先順位付けを提案する必要があります。合理的な上限 — 更新操作50件+作成操作10件。
7日以上経過した操作で成功ゼロのものは、dead letter queueに移動されます。手動で分析してください:APIが変更され、エンドポイントが存在しなくなった可能性があります。自動クリーンアップ — HealthCheckタスクが毎日実行され、期限切れの操作を削除またはアーカイブします。
依存関係グラフ(DAG)を使用します:各操作には、送信前に完了する必要があるparentOperationIdのリストが含まれています。ORDER BY parentを使用したRoomクエリは、正しい順序で操作を返します。カスケード送信 — 各操作が完了したら、子操作がブロック解除されているかどうかを確認します。
ネットワーク損失をシミュレートするには、Android EmulatorのNetwork Less ToolまたはiOS SimulatorのNetwork Link Conditionerを使用します。オフラインモードでキューに操作を追加し、接続を復元し、すべての操作が送信されてサーバーで処理されたことを確認するテストを作成します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。