Offline-First Synchronization
Building resilient mobile applications requires an Offline-First Architecture. Instead of assuming constant network connectivity, an offline-first app treats the local database as the primary source of truth, queuing background write mutations and resolving state conflicts when connectivity is restored.
1. Offline-First Architectural Pipeline
βββββββββββββββββ 1. Optimistic Write βββββββββββββββββββββ
β User UI β βββββββββββββββββββββββββββββββββββββββββββββΊ β Local Database β
βββββββββ¬ββββββββ βββββββββββ¬ββββββββββ
β β
β 2. Read Local State Immediately β 3. Queue Action
βΌ βΌ
βββββββββββββββββ 4. Sync Mutation βββββββββββββββββββββ
β State Manager β βββββββββββββββββββββββββββββββββββββββββββββ β Mutation Outbox β
βββββββββββββββββ βββββββββββ¬ββββββββββ
β
5. Network Restored
βΌ
βββββββββββββββββββββ
β Remote Server β
βββββββββββββββββββββThe 4 Core Pillars
- Local Primary Source of Truth: UI widgets subscribe exclusively to local database queries (via
Streams orValueListenables). - Optimistic UI Updates: State changes are written locally and rendered on screen instantly without waiting for network ACK responses.
- Persistent Mutation Outbox: Pending write operations (
POST,PUT,DELETE) are serialized into a persistent SQLite/Hive table to survive app kills. - Background Sync Worker: Automatically drains the outbox queue when internet connectivity transitions from offline to online.
2. Persistent Mutation Outbox Pattern
To prevent data loss when a user performs mutations offline and force-closes the app, the outbox queue must be persisted to disk:
class PendingMutation {
final String id;
final String action; // e.g. 'UPDATE_USER_PROFILE'
final Map<String, dynamic> payload;
final DateTime createdAt;
final int retryCount;
PendingMutation({
required this.id,
required this.action,
required this.payload,
required this.createdAt,
this.retryCount = 0,
});
Map<String, dynamic> toMap() => {
'id': id,
'action': action,
'payload': jsonEncode(payload),
'createdAt': createdAt.toIso8601String(),
'retryCount': retryCount,
};
}Exponential Backoff with Jitter
When retrying failed outbox mutations, calculate delays using exponential backoff with randomized jitter to prevent server stampedes:
Delay = 2^(retryCount) * 1000ms + RandomJitter(0..500ms)3. Conflict Resolution Strategies
When multiple clients modify the same record offline, synchronization can produce data conflicts.
1. Last-Write-Wins (LWW)
- Mechanism: Overwrites server data using the client timestamp (
updated_at). - Limitation: Vulnerable to client clock skew! If a userβs phone clock is set 5 minutes into the future, their updates overwrite newer server data indefinitely.
2. Conflict-free Replicated Data Types (CRDTs)
- Mechanism: Data structures mathematically designed to merge concurrent updates without central coordination or conflict errors.
- Types:
- LWW-Element-Set: Sets with timestamp-based additions and removals.
- PN-Counter: Counters that track independent increments and decrements.
3. Server-Authoritative Merge
- Mechanism: The server compares incoming payload version numbers (
version_id). If mismatched, the server rejects the write, computes a merged field-level payload, and returns the merged object for client reconciliation.
4. Trade-offs & Production Considerations
- Optimistic UI Rollbacks: If an optimistic update fails permanently on the server (e.g. 403 Forbidden or validation error), the client app must roll back local database changes and notify the user gracefully.
- Outbox Queue Bloat: If an app remains offline for weeks, the outbox queue can grow large. Cap max outbox entries and implement compaction policies to discard superseded duplicate operations.