| name | sync-strategies |
| description | Mobile sync strategies - pull, push, delta, bidirectional, event log, and CRDT - with tradeoffs, payload shapes, and cross-platform snippets. Use when designing how a mobile client stays consistent with a server. |
Sync Strategies
Instructions
Sync is the hardest part of offline-first. Pick the simplest strategy that meets the product's consistency needs. Do not default to CRDTs because they are fashionable.
1. Strategy Menu
| Strategy | Direction | Good for | Cost |
|---|
| Pull (poll) | Server -> client | Read-heavy, infrequent changes | Latency, wasted requests |
| Push (server-initiated) | Server -> client | Real-time notifications | Infrastructure (APNs/FCM/WS) |
| Delta sync | Server -> client | Large datasets with incremental updates | Requires server cursors |
| Outbox push | Client -> server | Writes while offline | Ordering and retry logic |
| Bidirectional | Both | Collaborative features | Conflict resolution |
| Event log / CRDT | Both, merge-friendly | Collaborative + offline + conflict-heavy | Engineering complexity, payload size |
Most apps need pull + outbox. A subset need delta. Real-time collab needs bidirectional. CRDTs are reserved for cases where conflicts are the norm.
2. Pull Sync
Simple and robust. The client asks for "things changed since X".
GET /articles?updatedSince=2026-04-19T12:00:00Z&limit=200&cursor=abc
- Use a monotonic cursor (opaque) rather than a timestamp when clock skew matters.
- Cap page size; iterate until empty.
- Store the cursor per entity kind on the device.
- Revalidate only what is visible or recently used; full-table pulls are rare.
3. Push Sync
Server tells the client when to sync.
- Transport: silent push (APNs/FCM data), websocket, SSE.
- Payload contains the minimal hint (
{type:"sync", entity:"articles"}), not the full change.
- The client then performs a delta pull.
- Silent push is throttled; always fall back to periodic pull.
4. Delta Sync
When the dataset is large, send only what changed.
{
"changes": [
{ "op": "upsert", "entity": "article", "id": "a1", "rev": 12, "body": {...} },
{ "op": "delete", "entity": "article", "id": "a2", "rev": 3 }
],
"nextCursor": "abc123",
"hasMore": true
}
- The server assigns monotonically increasing revisions per entity.
- The client applies ops in order within a transaction.
- Use batching (typically 100-500 ops per page) so UI updates are perceptible, not jittery.
5. Outbox Push
Writes go to a local outbox:
CREATE TABLE outbox(
id TEXT PRIMARY KEY,
created_at INTEGER NOT NULL,
entity TEXT NOT NULL,
op TEXT NOT NULL CHECK (op IN ('create','update','delete')),
payload BLOB NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT
);
- Idempotent ops. Each outbox row has a client-generated UUID; the server dedupes on retry.
- Exponential backoff with jitter; cap attempts; park-and-surface after the cap.
- Drain on app foreground, connectivity regained, or periodic background work.
- Never reorder within a single entity's history; between entities, parallelism is fine.
suspend fun drainOutbox() {
outboxDao.pending().forEach { row ->
try {
api.apply(row)
outboxDao.delete(row.id)
} catch (e: RetryableException) {
outboxDao.markFailure(row.id, e.message)
} catch (e: PermanentException) {
conflictDao.record(row, e)
outboxDao.delete(row.id)
}
}
}
6. Bidirectional Sync
Combine delta pull with outbox push. Core concerns:
- Versioning: each entity has a server revision. The client sends
baseRev with writes.
- Conflict: if the server has moved on, it rejects or returns a conflict payload.
- Resolution: see
conflict-resolution skill.
- Eventual consistency: the UI must tolerate a brief window where client and server disagree, marked as "pending" or "syncing".
7. Event Log / CRDT
Use when multiple devices edit the same document and conflicts cannot be avoided.
- Event log: client sends append-only events with a logical clock; server orders them; all clients rebuild state by replay.
- CRDT: conflict-free replicated data types (RGA for text, OR-Set for sets). Libraries: Yjs, Automerge, Fluid.
Cost: payloads are larger, merges are probabilistic in ordering, and debugging is harder. Justify with product need.
8. Scheduling
- On foreground: revalidate visible screens, drain outbox.
- On push: revalidate the hinted entity.
- Periodic:
WorkManager on Android (PeriodicWorkRequest), BGAppRefreshTask on iOS. Keep to minutes-hour cadence.
- On connectivity regained: drain outbox, not a full refresh.
9. Observability
Track:
- Outbox length, age of oldest pending write, failures by class.
- Delta pull duration and page count.
- Conflict counts per entity.
- Sync battery cost via platform tools (Battery Historian, Instruments).
10. Anti-Patterns
- Polling every few seconds while in foreground. Use push or longer intervals.
- Full-table pulls on every launch.
- Writing directly to the network without an outbox.
- Ordering guarantees that cross entities without justification.
- Relying solely on silent push for reliability.
- Adding CRDTs before proving conflicts are frequent.
Checklist