Офлайн-синхронизация в мобильном приложении — паттерны
Пользователь правит данные без сети, потом ловит метро или вай-фай — и тут начинается. Разбираем 4 паттерна офлайн-синха: last-write-wins, CRDT, очередь команд, event sourcing.
Курьер открывает приложение в лифте, отмечает заказ доставленным. Сеть появляется через 5 минут — а между делом он отметил ещё три. На сервере к этому моменту менеджер успел переназначить один из заказов. Чьи изменения побеждают? Это и есть офлайн-синхронизация — задача, у которой нет универсального решения. Разбираем четыре рабочих паттерна.
Базовые вопросы перед выбором
- Кто источник истины? Сервер всегда прав, клиент всегда прав, или это договорённость по полям.
- Что может редактироваться офлайн? Чем меньше — тем проще. Многие конфликты решаются ограничением области офлайн-правок.
- Какие конфликты допустимы? Потеря изменения, дубль, неверная сумма — где какой риск критичный.
- Multi-device. Один и тот же пользователь с двух устройств одновременно — отдельная история.
Паттерн 1. Last-write-wins (LWW)
Самый простой. Каждой записи — timestamp последнего изменения. Кто позже — побеждает. Реализация в 50 строк.
Когда работает. Заметки, профиль пользователя, настройки. Поля независимы, потеря промежуточного значения не критична.
Грабли.
- Часы устройств расходятся. Лучше использовать HLC (Hybrid Logical Clock) или серверное время с поправкой.
- Потеря изменений: если два устройства отредактировали поле, изменение того, кто пришёл «раньше», теряется молча.
- Не работает для коллекций (добавили элемент в список с двух устройств — один потеряется).
Паттерн 2. Очередь команд (Command Queue)
Клиент не правит данные напрямую — он генерит команды: «отметить заказ #1234 доставленным», «добавить позицию в чек». Команды копятся в локальной очереди, при появлении сети — отправляются по порядку с retry и idempotency-key.
Когда работает. CRUD-операции, где важна последовательность, и сервер — источник истины. Курьерские приложения, торговый агент в полях, кассы.
Реализация.
- Каждая команда — JSON с уникальным `client_id` (UUID v4) для дедупликации.
- Сервер хранит таблицу обработанных `client_id` — повторный запрос возвращает кэшированный результат.
- На клиенте — статус каждой команды: `pending`, `sent`, `confirmed`, `failed`. Пользователь видит, что синхронизация идёт.
- Конфликт (сервер вернул 409) — команда переходит в `failed`, UI показывает «требует разбора».
Грабли.
- Команда базируется на данных, которые на сервере уже изменились. Включать в команду version (optimistic locking) того, что правим.
- Очень длинная очередь после долгого офлайна — отправлять батчами, не по одной.
- UI должен отрабатывать optimistic update — иначе пользователь жмёт кнопку и ничего не происходит.
Паттерн 3. CRDT (Conflict-free Replicated Data Types)
Структуры данных, которые мерджатся автоматически без конфликтов. Любые операции коммутативны: порядок не важен, результат одинаков. Реализации: Yjs, Automerge, Loro.
Когда работает. Совместное редактирование документов, многопользовательские заметки, канбан-доски с офлайн, рисование. Реальное применение — Linear, Notion, Figma (отчасти).
Плюсы. Multi-device, multi-user без центрального сервера. Конфликтов нет по построению.
Минусы.
- Размер. CRDT хранит историю операций или версии. Документ в 10 МБ текста = CRDT в 50–200 МБ. Garbage collection помогает, но требует осторожности.
- Сложность. Программисты должны мыслить в терминах CRDT, а не в терминах «записал поле». На вход проекту — кривая обучения 1–2 недели.
- Не для денег. CRDT — про мерж, не про «сумма должна сойтись». Финансовые транзакции — отдельная история.
Паттерн 4. Event sourcing на клиенте
Клиент пишет события, которые синхронизируются с сервером. Сервер хранит лог событий и реконструирует состояние. Похоже на очередь команд, но события — это факты, а не намерения: не «отметить доставленным», а «отмечено доставленным в 14:32:05».
Когда работает. Системы с аудитом и историей: банковские, складские, медкарты. Возможность откатить, заглянуть в любой момент времени.
Грабли.
- Сложнее в реализации, чем командная очередь. Каждое изменение состояния — отдельное событие со схемой.
- Версионирование событий — событие 2025 года должно читаться кодом 2026.
- Хранение событий растёт линейно. Снэпшоты каждые N событий.
Что брать для типового приложения
- Доставка / выездной торговый агент / касса. Очередь команд + idempotency. Сервер — источник истины, optimistic UI.
- Заметки / задачи / профиль. LWW с HLC. Просто, легко.
- Совместная работа над документом / доской. CRDT (Yjs или Automerge).
- Финансы / медкарты / аудит. Event sourcing.
Слой хранения на клиенте
- SQLite + миграции. Дефолт. SQLDelight, Room, GRDB — типовые обёртки.
- WatermelonDB / RxDB. Готовые offline-first DB с механикой синхронизации. Хорошо для React Native / Cordova.
- PowerSync / ElectricSQL. Серверные движки sync поверх Postgres. Берут на себя дельты и конфликты. Платно, но экономят месяцы.
Что тестировать обязательно
- Холодный старт без сети.
- Длинный офлайн (24+ часа) с большой очередью.
- Сеть пропала в момент отправки — клиент должен повторить безопасно (idempotency).
- Конфликт версий: один и тот же объект изменён на сервере и на клиенте.
- Multi-device: одно и то же действие с двух устройств.
- Падение приложения посреди синхронизации — очередь не должна потеряться.
Итог: «офлайн-первое» — это архитектурное решение, а не фича. Команды + идемпотентность закрывают 80% бизнес-приложений. CRDT — для совместного редактирования. LWW — для простого. Event sourcing — для аудита. Главное — определиться с источником истины до того, как написана первая строка.