How would you make a mobile app work offline and sync reliably?
System designSenior
In shortMake the local database the UI's source of truth, record writes in an outbox, push them idempotently with client-generated ids, pull changes since a server-issued cursor including deletions, and resolve conflicts with versions rather than device clocks.
Model answer
- Reads and writes go to the local database (Room, SwiftData). The UI observes it, so it works offline and updates instantly.
- Writes are also recorded in an outbox in the same local transaction. A sync worker pushes it when there’s a network, in order, with client-generated UUIDs and idempotent requests (
PUT /items/{id}or anIdempotency-Key), so retries after lost responses don’t duplicate. - Pulls use a server-issued, commit-ordered cursor:
GET /changes?cursor=…returns created, updated and deleted items (tombstones), and the new cursor is saved in the same transaction as the applied changes. - Conflicts are detected with versions (each write carries the version it was based on) and resolved per field: last write wins by server order for low-stakes fields, merges for independent fields, domain rules where they exist, CRDTs for collaborative text. Make outcomes visible to the user.
- Platform: WorkManager with network constraints and backoff on Android; sync on foreground plus
BGAppRefreshTaskon iOS; real-time channels (WebSocket, silent push) only trigger the normal pull.
Senior follow-ups: tombstone retention and full resyncs, migrating unsynced outbox entries across app versions, partial sync for large datasets, and monitoring outbox size and sync failures per app version.
What they're looking for
The local-first data flow, idempotency under retries, cursors and tombstones, a conflict strategy with concrete rules, and platform realities (WorkManager, iOS background tasks). Bonus for discussing per-field merges and when CRDTs are worth it.
Common mistakes
- Network-first screens with a cache as an afterthought.
- Server-generated ids with POST retries, causing duplicates.
- Timestamp cursors and device-clock last-write-wins.