← All topics/Data persistence

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

Make the local database the single source of truth for the UI. Users read and write local state immediately, and a sync layer runs in the background to reconcile with the server whenever it can.

Represent pending mutations with an outbox, a persisted queue of operations that survives crashes and can be replayed. Each record carries the operation type, entity ID, payload, and retry state, so backoff and recovery are deterministic.

Define a conflict policy per entity type. Start simple: server wins for most fields, with merge rules for anything user-editable. Use an updatedAt timestamp or an ETag (entity tag) to detect when a conflict has happened.

Principles

  • Make every sync operation idempotent, with stable operation IDs so applying a mutation twice can't double its effect.
  • An outbox keeps offline writes crash-safe, because a queued mutation survives a restart and replays later.
  • Keep sync state separate from domain data, and track pending, failed, and synced status explicitly.
  • Decide the conflict policy up front, rather than letting the last write win by default.

An outbox record captures everything needed to replay or retry a mutation after a crash:

Outbox record sketch
struct PendingMutation {
    let opId: UUID
    let entityId: String
    let type: MutationType   // create, update, or delete
    let payload: Data
    var attemptCount: Int
}

Follow-up angles

  • Show sync errors clearly in the UI, with a badge or a retry control, so failures are visible and recoverable.