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

Source: https://www.techinterview.org/post/3233474986/design-offline-first-mobile-app/
Updated: 2026-07-26 · techinterview.org

"Design a mobile app that works fully offline" is a common [system design](/system-design-interview-guides/) 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 — every read hits local storage and every write commits locally first, so the UI never blocks on the network, even in airplane mode. Interviewers probe whether you understand that the local database, not the server, is the primary read path.

- Sync to cloud when network returns — queue local changes and flush them in the background once connectivity comes back. The user should never wait on a spinner for sync to finish.

- Resolve conflicts when the same record was edited on two devices — decide up front whether the last edit wins, the fields merge, or the user is asked to choose. State the rule per field so the behavior is predictable and testable.

- Multi-device support — same user on phone and tablet, each keeping its own local copy and syncing independently through the server. The case to reason about out loud is both devices going offline, diverging, then reconnecting.

- Eventually consistent across devices — you are not promising real-time consistency, only that after enough sync cycles every device converges to the same state. That convergence guarantee is exactly what LWW and CRDTs are chosen to provide.

## Storage choices

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

**Realm** is object-oriented with sync built in. The tradeoff is vendor lock-in.

**Document store** options (RxDB, Couchbase Lite) are schemaless and 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](/post/3233460997/system-design-collaborative-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 `lastSyncedAt` per 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=1` flag)

- Server merges and returns canonical versions

- Client clears `dirty` flags

## Battery and network

- Sync triggers: app foreground, push notification "data available", periodic background fetch. Modern mobile OSes throttle background fetch aggressively, so treat foreground and push as your reliable triggers and periodic fetch as best-effort.

- Compress payloads (gzip) — sync bodies are mostly text and typically shrink by more than half, which cuts both bandwidth and the radio-on time that drains the battery.

- Diff-only sync, never re-upload entire DB — send only records changed since `lastSyncedAt`, so payload size scales with edits made, not total data stored.

## Frequently Asked Questions

### Should I store data in JSON or relational?

For interview answers, [relational](/post/3233459967/sql-vs-nosql/) with a proper schema. Easier to reason about [indices](/post/3233461821/database-indexing-interview-guide/), 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).
