アプリを として扱う。各デバイスはローカルな である。学習アクションは と追記専用の (outbox)に書き込まれ、再接続時にデバイスは し、前回の同期以降の する。コンフリクトは単一のグローバルなルールで 、 によって解決する。
メンタルモデル: git のように考える。各デバイスはオフライン中にローカルで「commit」し、同期は上書きではなく merge である。
Device (offline) Server = source of truth
┌──────────────────┐ on reconnect ┌───────────────────────────┐
│ Local DB (SQLite)│ push ops (delta) │ Sync API /sync │
│ + Op log (outbox)│ ─────────────────▶ │ • idempotent by op.id │
│ cursor = lastSeen│ ◀───────────────── │ • merge by semantics │
└──────────────────┘ pull changes>cursor└───────────────────────────┘
Progress DB (Postgres)
スマホの時計は 信頼できず、2 台のデバイスが同じレコードをオフラインで編集し得るため、「last write wins」(LWW)は静かにデータを失う。semantics に基づいて解決する:
// op には id がある -> 冪等に適用(リトライで二重カウントしない)。clientTs は信頼しない。
function mergeProgress(server, op) {
return {
// progress は増加のみ -> max: 遅れて届いた古い op が後退させることはない
progress: Math.max(server.progress ?? 0, op.progress),
// studiedMs は加算カウンター -> delta を加える、上書きしない
studiedMs: (server.studiedMs ?? 0) + op.studiedMs,
completedAt: server.completedAt ?? (op.progress >= 1 ? op.clientTs : null),
};
}
ここで素の LWW は、遅れて progress: 0.3 で同期する古いデバイスにサーバーの 0.9 を 上書き させてしまう → progress の喪失。max を取り studiedMs を合算すれば同期は 順序に依存しない ものになり、何百万台ものデバイスがずれた時計で同期しても安全になる。
面接官は、あなたが クライアントの時計は信頼できない(だから純粋な timestamp LWW ではない)ことを知っているか、同期を idempotent にする(op id)か、データの意味に基づいて merge するかを見ている。弱い 回答:「timestamp を使って last write wins」。強い 回答: replica + op log + idempotency + フィールドごとの merge + source of truth としてのサーバー — そして LWW の データ損失 という失敗を明示的に指摘すること。
LWW は安いが失いやすい。semantic merge はフィールドごとの検討が要るが 何も失わない。真の共同編集(notes、documents)が必要なときだけ完全な CRDT/OT に手を伸ばす — コストが高いので、すべてに使ってはいけない。
ジュニアからシニアまで、詳細な回答付きのIT面接質問ライブラリ。
寄付する