Question 4
Offline support and syncing
Your app needs to work offline and sync with a backend. How do you design your persistence layer to handle conflicts and ensure consistency?
Follow-ups
- How do you represent pending writes?
Answer outline
Use the local database as the source of truth for UI. Users interact with local state immediately; a sync layer runs opportunistically in the background to reconcile with the server.
Represent pending mutations with an outbox — a persisted queue of operations that survive crashes and can be replayed. Each record carries the operation type, entity ID, payload, and retry state, making backoff and recovery deterministic.
Define a conflict policy per entity type. Start simple: server wins for most fields, with merge rules for anything user-editable. Use updatedAt timestamps or ETags to detect when a conflict has occurred.
Principles
- Make sync operations idempotent — applying the same mutation twice must not double the effect; use stable operation IDs to deduplicate.
- The outbox pattern — a persisted queue of mutations — is what makes offline writes crash-safe and retryable.
- Separate sync state from domain data — track pending, failed, and synced status explicitly.
- Define a conflict policy upfront; don’t rely on whichever write arrives last.
An outbox record captures everything needed to replay or retry a mutation after a crash:
struct PendingMutation {
let opId: UUID
let entityId: String
let type: MutationType // create/update/delete
let payload: Data
var attemptCount: Int
}
Follow-up angles
- Show clear UI for sync errors (badge/retry) so failures are visible and recoverable.