“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).
Recommended approach for an interview
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
- Client tracks a
lastSyncedAtper table - On sync trigger (foreground, push, periodic), client sends “give me changes since X”
- Server returns batch of records updated after X
- Client applies merge logic per record
- Client uploads its own pending changes (those with
dirty=1flag) - Server merges and returns canonical versions
- Client clears
dirtyflags
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).