プロンプトと対象範囲
このフロントエンドシステム設計の設問は、状態管理と分散更新を組み合わせたものです。単にキャッシュを追加するのではなく、ローカルデータ、ミューテーションキュー、サーバーバージョン、および可視化された復元状態を結びつけた回答が求められます。
面接官が見ているポイント
- ローカル下書き、保留中のミューテーション、サーバー確定状態の分離。
- コンフリクト戦略の選択と、自動マージできないフィールドの特定。
- リトライ、重複送信、ブラウザの終了、複数タブの処理。
- オフライン、同期中、コンフリクト、エラーの各状態を理解しやすく、復元可能にすること。
確認すべき明確化のための質問
各フィールドが独立しているか、金額などの監査対象や高リスクなフィールドがあるかを確認します。添付ファイルの有無、サーバーのバージョン管理や操作ログのサポート、Last-Write-Wins(最終書き込み優先)が許容されるか、ブラウザの対応状況、ローカル保持の要件などを確認します。
30秒で答える要約
耐久性のあるローカルストアとしてIndexedDBを使用し、すべての編集をclientMutationIdとベースのサーバーバージョンとともに記録します。UIは即座にローカルの結果を表示し、保留中(pending)としてマークします。オンライン復帰時にアプリまたはService Workerがキューを送信します。サーバーはバージョンチェックと冪等性(べきとう性)を用いて、成功、コンフリクト、またはバリデーションエラーを返します。安全なフィールドは自動マージ可能ですが、高リスクなフィールドは明示的な選択のために差分(diff)を表示します。すべての状態がリトライまたは取り消し(Undo)をサポートします。
ステップ別の詳細解説
1. 最初に状態モデルを定義する
各下書きはserverVersion、localVersion、syncState、lastErrorを持ちます。保留中の各ミューテーションは、clientMutationId、フィールドの変更内容、作成日時、ベースバージョンを保持します。読み取りはローカルストレージから行い、ネットワークレスポンスはバージョンチェック後にのみマージされるため、古いレスポンスが新しい編集を上書きすることはありません。
2. 復元可能なファクトをIndexedDBに永続化する
IndexedDBは、より大きな構造化オフラインデータに適していますが、設計にはスキーマのアップグレード、クォータ(容量制限)エラー、ブラウザによる退避(eviction)への対応を含める必要があります。下書き、ミューテーション記録、コンフリクトを別々に保存し、下書きとキュー記録を1つのトランザクションでコミットします。機密フィールドは最小限に抑え、ログアウト時や有効期限切れ時に消去します。
3. 同期トリガーと冪等性を設計する
オンライン時は即座に試行し、オフライン時はキューに入れます。Background Syncがサポートされている環境では、Service Worker用のユニークなタグを登録しますが、このAPIを万能な配信保証として扱ってはなりません。すべてのリクエストにclientMutationIdとベースバージョンを含め、同じIDを再送した場合はミューテーションを重複適用せずに同一の結果を返すようにします。
4. コンフリクトポリシーをフィールドのリスクに合わせる
ラベルなどの独立したフィールドは、フィールドごとのバージョンでマージできます。金額、権限、コンプライアンス関連の記述などは、暗黙的にLast-Write-Winsを適用すべきではありません。コンフリクト発生時は、ローカル値、サーバー値、編集時刻を並べて表示し、ユーザーの選択によって新しいミューテーションを作成します。自動マージの場合はその入力内容を記録する必要があります。
5. 失敗、リトライ、複数コンテキストの処理
ネットワーク障害に対しては、キューの順序を維持しながら指数バックオフ(exponential backoff)を使用します。バリデーションエラーは、無限リトライではなく編集可能なエラー状態とします。複数タブ間はBroadcastChannelやバージョンチェックを通じて調整し、古いミューテーションが重複送信されないようにします。耐久性のあるキューは次回起動時に再開し、UIには保留中の件数と最後のエラーを表示します。
優れた回答例
私ならまずフィールドのリスクとオフラインの境界を定義します。クライアントはIndexedDBに下書きとミューテーションキューを保存します。各ミューテーションはclientMutationIdとベースのserverVersionを持ち、UIは即座にローカルの結果を保留中として表示します。アプリまたはService Workerが再接続後にキューを送信し、サーバーはバージョンチェックと冪等性を用いて成功、コンフリクト、またはバリデーションエラーを返します。独立したフィールドは自動マージできますが、高リスクなフィールドはローカル対サーバーの差分を表示し、確認を求めます。ネットワークエラーはバックオフし、ビジネスロジックのエラーは再編集可能にします。複数タブはバージョン通知を使用して、古いデータが新しい作業を上書きしないようにします。インターフェースはオフライン、同期中、コンフリクト、失敗、保存済みの各状態を明確に区別します。
よくある間違い
- ミューテーションキューやベースバージョンを持たず、最新のフォームのみをキャッシュすること。
- タイムスタンプやLast-Write-Winsをすべてのフィールドに一律適用すること。
- Background Syncをすべてのブラウザで保証されたものとして扱うこと。
- 冪等性キーなしでリトライし、重複データを作成してしまうこと。
- コンフリクトをコンソールに出力するだけで、ユーザー向けの復元手段を用意しないこと。
- スキーマアップグレード、クォータ、複数タブ、ブラウザの終了を無視すること。
フォローアップ質問と回答
2つのタブが同時に編集した場合はどうなりますか?
各タブがバージョン変更をサブスクライブし、ベースが古くなった場合にマージを要求します。送信前にバージョンを再チェックします。サーバー側でも共有ミューテーションIDによって重複排除が行われます。
オフラインデータが漏洩する可能性はありますか?
ローカルには必要なフィールドのみを保存し、機密コンテンツを暗号化し、保持期間を短縮し、ログアウト時、共有デバイスの使用時、またはポリシー変更時に消去します。
ブラウザがBackground Syncをサポートしていない場合はどうしますか?
アプリの起動、フォーカス取得、接続状態の変更、ショートポーリングをフォールバックのトリガーとして使用し、レコードが保留中であることをユーザーに伝えます。システムの正しさがこのAPIに依存してはなりません。
自動マージが安全なのはどのような場合ですか?
フィールドのセマンティクス(意味構造)が独立しており、バージョンのベースが判明していて、一時的な不一致が許容される場合に限られます。金額、権限、在庫、監査フィールドは明示的な確認を必須とすべきです。