Мобильная сеть ненадёжна: метро, лифт, роуминг, переключение Wi-Fi. Поэтому «как приложение работает без сети» это один из главных вопросов мобильного собеса, особенно в system design.
Единый источник правды
В offline-first приложении источник правды для UI это локальная база (Room, Core Data или SwiftData, SQLite, Drift, Realm). Экран подписан на базу, а сеть только обновляет её:
сеть → репозиторий → база → (подписка) → view model → UI
Поведение одинаково с сетью и без, нет гонок «что показать: кеш или ответ».
Чтение
- Показать то, что есть в базе, сразу.
- Параллельно запросить изменения с сервера.
- Записать их в базу, UI обновится сам.
Для больших наборов нужна дельта-синхронизация: клиент передаёт курсор последней синхронизации, сервер отдаёт изменённые записи и удаления. Удаления передаются «надгробиями» (tombstones): иначе клиент, который был офлайн во время удаления, не узнает о нём.
Запись: outbox
пользователь → база (сразу) + операция в outbox (в одной транзакции)
синхронизатор → отправка операций по порядку → удаление из outbox после подтверждения
- Изменение видно сразу (оптимистичный UI).
- Очередь на диске переживает выгрузку процесса и перезагрузку.
- У каждой операции ключ идемпотентности: если ответ потерялся и запрос повторился, сервер не создаст дубль.
- Ошибки повторяются с экспоненциальной задержкой; «вечные» ошибки (валидация) показываются пользователю.
Когда синхронизировать
Система сама решает, когда запускать фоновую работу: WorkManager на Android, BGTaskScheduler и фоновые URLSession на iOS. Точного времени нет. Поэтому синхронизация запускается также:
- при старте приложения и возврате на передний план;
- при появлении сети;
- по тихому push от сервера.
Постоянный фоновый поток система убьёт.
Конфликты
Два устройства изменили одну запись офлайн. Стратегии:
- Last write wins по объекту или по полям: просто, но время устройства ненадёжно, а перезапись объекта теряет параллельные изменения разных полей.
- Версии и оптимистичная блокировка: клиент отправляет версию, на основе которой менял; если на сервере новее, ответ 409, клиент скачивает актуальное и сливает.
- Слияние по полям: менялись разные поля, конфликта нет.
- Сервер всегда прав: для денег и остатков.
- Ручное разрешение: показать обе версии.
- CRDT (Automerge, Yjs) для совместного редактирования текста и списков: изменения сходятся при любом порядке применения.
Выбор по цене потери изменения: настройки переживут LWW, текст заметки нет.
Статус для пользователя
Пометки «отправляется», «не отправлено, повторить», индикатор офлайна, время последней синхронизации. Как часики у неотправленного сообщения в мессенджере.
Подводные камни
- Миграция схемы базы при неотправленных операциях в outbox.
- Первая синхронизация большого аккаунта: постранично, сначала нужное сейчас.
- Отставшие дольше срока хранения надгробий клиенты: полная пересинхронизация.
- Выход из аккаунта: что делать с неотправленными изменениями (предупредить).
Частые ошибки
- Очередь отправки в памяти.
- Параллельные синхронизаторы, отправляющие одно и то же.
- Сравнение времени устройства с серверным.
- UI, который показывает то кеш, то ответ сервера, и мигает.
Что спросят на собесе
- Что такое offline-first и единый источник правды?
- Как надёжно доставить изменения, сделанные без сети?
- Что такое идемпотентность и зачем ключ на клиенте?
- Как разрешать конфликты между устройствами?
- Как синхронизировать большой набор данных эффективно?