Design an Offline-First Mobile App: Sync, Conflicts, and CRDTs

“Design a mobile app that works fully offline” is a common system design prompt. Real-world examples: notes apps, todo apps, journaling, drawing tools, mobile email. The interview is testing whether you understand the implications of treating the device as the source of truth and reconciling with the cloud later.

Functional requirements

  • Read and write all data while offline
  • Sync to cloud when network returns
  • Resolve conflicts when the same record was edited on two devices
  • Multi-device support — same user on phone and tablet
  • Eventually consistent across devices

Storage choices

SQLite is the default. Mature, fast, ubiquitous. Pair with a thin ORM (Room on Android, GRDB on iOS).

Realm: object-oriented, sync built in. Tradeoff: vendor lock-in.

Document store (RxDB, Couchbase Lite): schemaless, sync-friendly.

Sync strategies

Last-writer-wins

Simplest. Each record has a updatedAt. On sync, the device with the larger timestamp wins. Pitfall: clock skew. Use a hybrid logical clock (HLC) to combine wall time with logical sequence.

Operational transformation (OT)

Used by Google Docs. Operations are transformed against concurrent edits to converge. Complex; usually overkill for mobile-only apps.

CRDTs

Conflict-free replicated data types. Mathematical guarantee of convergence under any merge order. Yjs and Automerge are the popular libraries. Cost: more storage overhead per record (vector clocks, op logs).

For a notes app: per-field LWW with HLC timestamps. For collaborative real-time editing: CRDT (Yjs). For a todo app where order matters: fractional indexing for ordering, LWW for fields.

Tombstones and deletions

Soft deletes only. A deleted record becomes a tombstone with a deletion timestamp. Tombstones are garbage-collected after a TTL longer than the longest possible offline period (~30 days).

Sync protocol

  1. Client tracks a lastSyncedAt per table
  2. On sync trigger (foreground, push, periodic), client sends “give me changes since X”
  3. Server returns batch of records updated after X
  4. Client applies merge logic per record
  5. Client uploads its own pending changes (those with dirty=1 flag)
  6. Server merges and returns canonical versions
  7. Client clears dirty flags

Battery and network

  • Sync triggers: app foreground, push notification “data available”, periodic background fetch
  • Compress payloads (gzip)
  • Diff-only sync — never re-upload entire DB

Frequently Asked Questions

Should I store data in JSON or relational?

For interview answers, relational with a proper schema. Easier to reason about indices, queries, and migrations. Use JSON columns for genuinely flexible blobs.

How do I handle schema migrations?

Bundled migration scripts that run on app launch. Each migration is idempotent and versioned. Always test on real devices with old data.

What about end-to-end encryption?

Encrypt at rest on the device (SQLCipher) and in transit (TLS). For E2EE between devices, exchange keys via a trusted setup (QR code or secret derivation from password).

Scroll to Top