Design a Notes App (Android) — Microsoft Interview
medium20 minAndroid System Design
How Microsoft Tests This
Microsoft system design interviews cover collaborative productivity tools, cloud infrastructure, messaging systems, and calendar/scheduling services. They test your ability to design enterprise-grade, highly available systems.
Designing a notes app might be the most underestimated question in Android system design interviews. Candidates hear "notes app" and think CRUD. Then the interviewer asks how you handle a note edited on two different devices while both were offline, and suddenly you're deep in conflict resolution strategies, vector clocks, and the hard tradeoffs between user experience and data safety.
This question shows up at Google, Microsoft, Notion, Dropbox, and pretty much any company building productivity tools on Android. It's popular precisely because it's a clean, bounded problem that happens to touch every hard Android topic at once: offline-first design, Room schema decisions, background sync with WorkManager, multi-device state reconciliation, and rich text storage.
The best candidates don't just describe a notes CRUD app — they treat it as a distributed systems problem running on a constrained mobile device.
Step 1: Clarify the Scope
Interviewer: Design a notes app for Android.
Candidate: Before I dive in — a few questions. Are we designing a local-only notes app or one that syncs across devices? If it syncs, do we need real-time collaboration, or is eventual consistency between the user's own devices acceptable? Do notes support rich text — bold, italics, lists, embedded images — or plain text only? Do we need folder organisation, tagging, or search? And what's the offline expectation — can the user create, edit, and read notes without network access?
Interviewer: The app syncs across the user's own devices. No real-time collaboration — just eventual consistency. Rich text support — at least bold, italic, bullet lists. Tags and search are in scope. Full offline support is a hard requirement. Assume Google Keep or Apple Notes as the reference.
Candidate: Perfect. Offline-first is the most important architectural constraint here — it shapes every single decision from schema design to sync strategy. Let me start with requirements and then walk through the architecture.
Requirements
Functional
Create, edit, and delete notes fully offline
Notes contain rich text (bold, italic, bullet lists) and optionally images/attachments
Tag notes for organisation; filter notes by tag
Full-text search across note content and title
Notes sync across the user's own devices when connectivity is available
Handle conflicts when the same note is edited on two devices while offline
Archive and pin notes
Non-Functional
Offline-first — all core operations must work without a network connection
Zero perceived latency — UI writes must feel instant; syncing happens in the background
No data loss — a note written offline must never be silently dropped
Battery-safe — background sync must not drain the battery
Conflict resilience — concurrent edits across devices must be resolved correctly and predictably
Back-of-the-Envelope Estimates
Interviewer: Give me a sense of the numbers.
Candidate: For a per-device estimate:
plaintext
Notes per user: ~500 average (active user)Average note size (text): ~5 KBNotes with attachments: ~10%Average attachment size: ~500 KB (photos, audio clips)Local storage (text only): 500 notes × 5 KB = ~2.5 MB — trivially small, no concernLocal storage (with attachments): 50 attachments × 500 KB = ~25 MB — manageable per deviceSync payload per session: Assume user edits 5 notes per day 5 × 5 KB = 25 KB delta upload — negligible bandwidthSearch index: Room FTS4 overhead is ~2× the text content size 500 notes × 5 KB × 2 = ~5 MB — fine for SQLite
The numbers confirm this is not a scale problem on the client. Notes apps don't have high write throughput or large data volumes per device. The interesting challenge is correctness — not performance. Specifically: how do you guarantee offline writes don't get lost, and what happens when two devices edit the same note while offline.
Client Architecture
Interviewer: Walk me through the overall architecture.
Candidate: Clean Architecture with MVVM, Room as the single source of truth, and WorkManager handling all background sync.
plaintext
UI Layer NoteListScreen / NoteEditScreen ← ViewModel (StateFlow / Compose State)Domain Layer Use Cases: CreateNote, UpdateNote, DeleteNote, SearchNotes, SyncNotesData Layer NoteRepository ├── Local Source → Room (all reads and writes, always) └── Remote Source → Retrofit (sync only, never on the critical path)Sync Layer NoteSyncWorker (WorkManager CoroutineWorker) SyncStatusTracker (exposes sync state to UI)
The critical principle: the UI never talks to the network directly. Every read and write goes through Room. The ViewModel observes Room via Flow — when a note is saved, Room emits the update and the UI reacts. Sync happens entirely in the background.
This is what makes offline feel seamless. When a user types a note with no internet, the experience is identical to when they have full connectivity. Room doesn't know or care about the network.
Room Schema Design
This is the first place interviewers probe in depth. A weak schema here cascades into problems with sync, conflict resolution, and search.
Interviewer: Walk me through the Room schema for a notes app.
Candidate: Let me walk through each entity and explain why each field exists.
kotlin
@Entity(tableName = "notes")data class NoteEntity( @PrimaryKey val id: String, // UUID — client-generated, not server auto-increment val title: String, val contentJson: String, // rich text serialised as JSON (covered below) val contentPlainText: String, // stripped plain text — for FTS indexing val isPinned: Boolean = false, val isArchived: Boolean = false, val color: String? = null, // hex colour string, nullable val createdAt: Long, // epoch millis, set on creation, never changes val updatedAt: Long, // epoch millis, updated on every local edit val serverUpdatedAt: Long? = null, // the server's last-modified timestamp — for conflict detection val syncStatus: SyncStatus, // SYNCED, PENDING, CONFLICTED val isDeleted: Boolean = false // soft delete — never hard-delete before sync)enum class SyncStatus { SYNCED, PENDING, CONFLICTED }
A few decisions to call out explicitly.
Client-generated UUID. If the primary key were a server-assigned integer, the client couldn't create a note offline — it would have no ID until the server responds. A UUID generated on the device means the note has an ID from the moment of creation, before any network interaction.
contentPlainText alongside contentJson. Rich text content is stored as JSON (explained below). But Room FTS can't tokenise JSON structure — it needs plain text. So we maintain a stripped version for search indexing.
syncStatus. This is the heartbeat of the offline-first design. Every note is in one of three states: SYNCED (local and server agree), PENDING (local change not yet sent to server), or CONFLICTED (a conflict was detected that needs resolution). The sync worker queries for PENDING notes; the UI surfaces CONFLICTED notes to the user.
Soft delete with isDeleted. Never hard-delete a note before it's synced. If you delete a note locally and it's never been synced, you need to inform the server on the next sync. A hard delete loses that signal.
Tags schema:
kotlin
@Entity(tableName = "tags")data class TagEntity( @PrimaryKey val id: String, val name: String, val userId: String)@Entity( tableName = "note_tags", primaryKeys = ["noteId", "tagId"])data class NoteTagEntity( val noteId: String, val tagId: String)
FTS virtual table for full-text search:
kotlin
@Fts4(contentEntity = NoteEntity::class)@Entity(tableName = "notes_fts")data class NotesFts( val title: String, val contentPlainText: String)
Room's @Fts4 annotation creates a SQLite full-text search virtual table backed by contentPlainText and title. FTS4 tokenises content at write time, enabling MATCH queries that are orders of magnitude faster than LIKE '%term%' on large note collections. We'll use this for the search feature.
Rich Text Storage
Interviewer: How do you store and render rich text in the note editor?
Candidate: This is an area where candidates often say "use a WebView" and move on. Let me give a more considered answer.
What it is: Android's native text system uses Spanned / SpannableString — in-memory objects that hold text alongside formatting ranges (bold from position 5 to 10, italic from 20 to 25, etc.). These are great for rendering in EditText but can't be directly persisted to Room.
The storage strategy: serialise the rich text document to JSON. The format stores the raw text plus an array of spans with their type, start, and end positions:
On write: convert SpannableString → JSON, store in contentJson. Also extract text as contentPlainText for FTS.
On read: deserialise JSON → rebuild SpannableString and display in EditText.
Markdown as an alternative: some notes apps (Notion, Bear) store content as Markdown strings. This is simpler to serialise and portable, but rendering Markdown in Android requires either a third-party library (Markwon is the standard) or a WebView. The trade-off: Markdown is easier to sync and diff across devices (it's just text), but native Markdown rendering is heavier than SpannableString. For a Google Keep-style app with basic formatting, JSON-serialised spans is the right call. For a Notion-style app with complex block structures, consider a block-based document model.
What not to do: never store HTML in the database. HTML is verbose, hard to diff, and the WebView it requires for rendering is expensive for list scrolling.
Offline-First Architecture: The Write Path
Interviewer: Walk me through what happens when the user creates a note with no internet connection.
Candidate: This is the critical path, and it's entirely local:
plaintext
User types in NoteEditScreen │ ▼ViewModel.saveNote() │ Called on a debounced timer (500ms after typing stops) ▼NoteRepository.save(note) │ ▼Room.insert/update(note.copy(syncStatus = PENDING, updatedAt = now())) │ Returns immediately — sub-millisecond ▼UI observes Room Flow → updates note list with "saving..." indicator │ ▼WorkManager enqueued with CONNECTED constraint (No-op if already enqueued for this note — idempotent enqueue)
The user sees their note saved instantly. There's no loading spinner, no "waiting for network" state. The PENDING status is invisible to the user at this point — it's an internal state that drives the sync pipeline.
The debounce is important. Notes apps don't save on every keystroke — that would flood Room with writes. A 500ms debounce means one Room write per burst of typing, not one per character. On process kill, the ViewModel's onCleared() triggers an immediate save of any pending debounced content so nothing is lost.
Background Sync: WorkManager
What WorkManager does: WorkManager is the Jetpack-recommended solution for background work that must complete, even if the app is killed or the device restarts. It persists work to a Room-backed queue, respects battery constraints, and retries with exponential backoff on failure.
Interviewer: How does sync work in the background?
Candidate: The NoteSyncWorker runs in three scenarios: when the user explicitly triggers a sync, when the network becomes available (via ConnectivityManager.NetworkCallback), and on a periodic schedule (every hour).
The worker runs as a CoroutineWorker — safe to do Room reads and API calls without blocking the main thread:
plaintext
NoteSyncWorker.doWork(): 1. Query Room for all notes where syncStatus = PENDING 2. For each PENDING note: a. Try POST/PUT to sync API with { id, contentJson, updatedAt } b. Server responds with { serverId, serverUpdatedAt, conflicted: bool } c. If no conflict: Room update: syncStatus = SYNCED, serverUpdatedAt = response.serverUpdatedAt d. If conflict: Room update: syncStatus = CONFLICTED Store server version alongside local version 3. Pull new notes from server (delta pull — send lastSyncTimestamp) For each server note: If note not in Room: INSERT (syncStatus = SYNCED) If note in Room and serverUpdatedAt > local serverUpdatedAt: Check for PENDING local changes → potential conflict 4. Return Result.success() or Result.retry() on transient failure
Enqueue the worker as unique work using ExistingWorkPolicy.KEEP. This prevents multiple simultaneous sync workers from racing each other:
For the periodic sync, use enqueueUniquePeriodicWork with ExistingPeriodicWorkPolicy.KEEP to prevent redundant periodic sync registrations across app restarts.
Conflict Resolution: The Hard Part
This is where the interview gets interesting, and where the most valuable answers live. A notes app syncing across devices will inevitably have conflicts — the same note edited on two devices while both were offline.
Interviewer: The user edits a note on their phone while offline. The same note was edited on their tablet while that was also offline. Both devices reconnect. What happens?
Candidate: This is the classic offline sync conflict, and there are three resolution strategies — each with different trade-offs.
Strategy 1: Last-Write-Wins (LWW)
Compare updatedAt timestamps. The version with the more recent timestamp wins; the other is discarded.
Simple to implement, zero user friction — but can silently discard work the user cares about. Acceptable for apps where notes are short and the cost of a lost edit is low. Not acceptable if a user writes a 500-word note on their phone and later fixes a typo on their tablet.
Strategy 2: Show Both — User Resolves
When a conflict is detected, mark the note as CONFLICTED in Room. Store both versions: the local version and the incoming server version. Surface the conflict in the UI — a "conflict detected" badge on the note list item. When the user taps it, show a diff view with both versions and let them choose which to keep, or manually merge.
This is the safest option for user data — nothing is lost. But it requires a UI for conflict resolution (a diff view is non-trivial to build) and puts cognitive load on the user.
Strategy 3: Auto-Merge (Best for Text)
If the conflicting edits touch different parts of the text — one user added a paragraph at the top, the other edited the middle — they can be automatically merged. Use a three-way merge algorithm: find the common ancestor (the last synced version), apply both sets of changes independently, and combine them.
If the edits genuinely conflict (both edited the same sentence differently), fall back to Strategy 2 and surface the conflict to the user.
What I'd recommend: Strategy 3 with a fallback to Strategy 2. Auto-merge handles the majority of real-world conflicts silently; user resolution handles the hard cases. The implementation uses a CRDT-inspired approach or a Myers diff algorithm for text merging. This is what Notion and Apple Notes approximate in production.
Interviewer: What's a CRDT and when would you use one?
Candidate: A Conflict-free Replicated Data Type is a data structure designed to be merged from multiple sources without conflicts — mathematical guarantees, not heuristics. Every individual character insertion/deletion is tracked as an operation with a logical clock. Merging two CRDT documents always produces the same result regardless of order.
The trade-off: CRDT-encoded documents are significantly larger (each character has metadata), more complex to implement, and require a CRDT-aware server. For a simple notes app, the three-way merge approach is simpler and correct for most cases. CRDTs make sense if you're building real-time collaborative editing where multiple users type simultaneously — like a Notion or Google Docs clone. For a single user's notes syncing across their own devices, last-write-wins or three-way merge is almost always sufficient.
Delta Sync: Don't Send Everything
Interviewer: How does the client know which notes to pull from the server on sync?
Candidate: Delta sync. Rather than downloading every note on every sync, the client sends its last successful sync timestamp and the server returns only what changed since then.
plaintext
Client → GET /sync?since=1733990400000&deviceId=abc123Server → { updated: [note1, note2], // created or modified since that timestamp deleted: ["note_id_5"] // deleted on another device since that timestamp}
The client stores lastSyncTimestamp in DataStore<Preferences> — not Room. It's a scalar value, not structured data. DataStore is the modern replacement for SharedPreferences and avoids the thread-safety issues that SharedPreferences has when accessed from multiple coroutines.
On the server side, every note update touches a updated_at server timestamp that the delta query uses. Deletions are soft-deleted and tracked in a deleted_notes table so they appear in the delta response before the record is permanently purged.
If you want to practice explaining exactly this kind of trade-off — why DataStore over SharedPreferences, why delta sync over full refresh — under interview pressure, Mockingly.ai has Android system design simulations that put you in exactly these conversations.
Full-Text Search with Room FTS4
Interviewer: How does search work?
Candidate: Room's @Fts4 annotation creates a virtual table backed by SQLite's FTS4 extension. FTS4 tokenises text at insert time and maintains an inverted index, enabling fast prefix and phrase matching.
What FTS4 is: SQLite's Full-Text Search extension creates a separate virtual table alongside the main notes table. When a note is inserted or updated, FTS4 tokenises the text and updates its index. Queries use MATCH syntax rather than LIKE, and the lookup is O(log n) against the index rather than a full-table scan.
The DAO query:
kotlin
@Daointerface NoteDao { @Query(""" SELECT notes.* FROM notes INNER JOIN notes_fts ON notes.id = notes_fts.rowid WHERE notes_fts MATCH :query AND notes.isDeleted = 0 AND notes.isArchived = 0 ORDER BY rank """) fun searchNotes(query: String): Flow<List<NoteEntity>>}
The caller appends * to enable prefix matching: "meet*" matches "meeting", "meetup", "meetings". Without the wildcard, FTS4 only matches exact tokens.
What Room FTS4 doesn't handle: fuzzy matching ("meetting" → "meeting"). If fuzzy search is important, consider maintaining a trigram index manually or preprocessing the query to expand common misspellings. For most notes apps, prefix matching is sufficient.
Attachments: Images and Files
Interviewer: How do you handle image attachments in notes?
Candidate: Never store image bytes in Room. SQLite is not an object store, and large blobs in a Room database degrade query performance for the entire database.
The pattern:
plaintext
User attaches photo to a note │ ▼Copy photo to app's internal private storage → /data/data/com.example.notes/files/attachments/{uuid}.jpg (Internal storage — not accessible to other apps, no storage permission needed) │ ▼Store only the local file path in Room: AttachmentEntity { id, noteId, localPath, remoteUrl, syncStatus } │ ▼On sync: upload file to remote storage (S3 / GCS) via pre-signed URL (same pattern as photo apps — direct upload, not through the app server) │ ▼Update AttachmentEntity.remoteUrl once uploaded
Why copy to internal storage? The original photo might live in the camera roll, accessible via a content URI. Content URIs are temporary and can be revoked if the user deletes the photo from the gallery. Copying to internal storage gives the notes app durable ownership of the attachment.
Attachment sync: attachments are uploaded asynchronously in a separate AttachmentSyncWorker. They don't block note text sync. Note text syncs fast (bytes); attachment upload can take seconds depending on file size and network speed. Separating them keeps note sync latency low and allows attachment upload to retry independently.
Storage quota: warn the user when attachment storage exceeds a configurable threshold (say, 500 MB). Offer a "clear cached attachments" option that deletes locally cached versions of remotely backed attachments — they can be re-downloaded on demand.
Sync Status UI
The UI should reflect sync state transparently without being intrusive.
Interviewer: How does the user know if their notes are synced?
Candidate: A SyncStatusTracker in the ViewModel exposes the aggregate sync state as a StateFlow:
kotlin
sealed class SyncState { object Synced : SyncState() object Syncing : SyncState() data class PendingCount(val count: Int) : SyncState() data class Conflict(val noteId: String) : SyncState() object Offline : SyncState()}
The notes list screen observes this state and renders accordingly:
Synced: no indicator shown — clean state is the default
Syncing: a subtle progress indicator in the toolbar
PendingCount: "3 notes waiting to sync" — visible but not blocking
Conflict: a badge on the conflicted note's list item — red dot
Offline: "Working offline" banner at the top, non-blocking
The key UX principle from offline-first design: optimistic UI. Show the note as saved immediately. The "syncing" state is a background concern. Never block the user from editing while sync is in flight. The phrase "Saved locally — syncing" is more reassuring than a spinner that blocks typing.
Handling Edge Cases
Interviewer: What happens if the user deletes a note on their phone while it was being edited on their tablet?
Candidate: The delete event gets a serverTimestamp when it syncs. On the next sync to the tablet, the server sends a deleted array containing that note's ID. The tablet's sync worker checks: does this note have local unsaved changes (syncStatus = PENDING)?
If no local changes: apply the deletion. Room soft-deletes the note.
If yes, local PENDING changes: surface a conflict to the user. "This note was deleted on another device, but you have unsaved changes here. Keep your version or confirm deletion?" This is a case where auto-merge can't help — explicit user decision is required.
Interviewer: What about the note the user created offline that has the exact same UUID as one already on the server?
Candidate: UUID collision probability with a standard UUID.randomUUID() (Version 4 UUID) is approximately 1 in 2^122. With a billion notes created, the probability of any two sharing a UUID is roughly 1 in 10^27. For practical purposes this never happens. But defensively, the sync API validates UUID uniqueness server-side and returns a 409 Conflict if a collision somehow occurs. The client generates a new UUID and retries. This is a theoretical concern worth acknowledging but not worth over-engineering.
Common Interview Follow-ups
"How would you implement real-time collaboration — two users editing the same note simultaneously?"
This changes the architecture significantly. You'd need a WebSocket connection to a backend that broadcasts operations in real time. The local note model would switch to a CRDT (Conflict-free Replicated Data Type) like Yjs or Automerge, where every character insertion/deletion is represented as an operation with a logical timestamp. Room still holds the local state, but the sync mechanism becomes operation-based (send operations, receive operations) rather than state-based (send full note, receive full note). This is a significantly heavier lift than eventual-consistency sync — worth naming the architectural difference clearly.
"How do you handle the case where WorkManager can't sync because the device has been offline for weeks?"
The server retains notes in a soft-deleted state for a grace period (say, 90 days) before permanent deletion. Notes flagged as deleted by another device more than 90 days ago will simply not appear in delta sync responses. The client's eventual sync catchup handles all changes within that window correctly. If a device reconnects after the grace period, the user might see stale notes that others consider deleted — surface this edge case honestly in the design.
"How would you add a sharing feature — send a note to another user?"
Shared notes are a different data ownership model. A note owned by User A can be granted read or write access to User B. Server-side: a note_shares table with { note_id, owner_id, recipient_id, permission }. Client-side: the shared note appears in the recipient's note list via delta sync. The recipient's edits sync to the server, which fans out to the owner's devices on their next delta pull. Conflict resolution applies across users just as it does across devices.
"How do you prevent data loss if Room's database gets corrupted?"
Room's database file lives in internal storage — not backed up by default by Android's automatic backup. Enable android:allowBackup="true" and android:fullBackupContent rules in the manifest to include the Room database in Google's automatic cloud backup (limited to 25 MB). For production: implement explicit export — a background task that periodically exports all notes to a JSON file in the user's Google Drive or via Android's BlobStoreManager. Defense in depth: if the local database is corrupted, a full re-download from the server is the recovery path. This is why the server is the durable source of truth, not the device.
Quick Interview Checklist
✅ Clarified scope — sync across devices, rich text, tags, search, full offline support
✅ Clean Architecture + MVVM, Room as single source of truth
✅ Client-generated UUIDs — offline note creation without server round-trip
✅ syncStatus enum on every note — SYNCED, PENDING, CONFLICTED
✅ Soft delete (isDeleted) — never hard-delete before sync
✅ updatedAt and serverUpdatedAt — local and server timestamps for conflict detection
✅ Rich text stored as JSON spans, plain text extracted separately for FTS
✅ Room FTS4 for full-text search — @Fts4 annotation, MATCH query, prefix wildcard
✅ Debounced saves in ViewModel — not on every keystroke, immediate save on onCleared()
✅ WorkManager CoroutineWorker for background sync — persists across process kills and reboots
✅ enqueueUniqueWork with KEEP policy — no duplicate sync workers
✅ Three conflict strategies explained — LWW, user resolution, auto-merge with fallback
✅ Delta sync with lastSyncTimestamp — not full re-download on every sync
✅ lastSyncTimestamp in DataStore (not SharedPreferences, not Room)
✅ Attachments copied to internal storage, file path in Room, uploaded separately
✅ AttachmentSyncWorker separate from note text sync — different latency/retry profiles
✅ SyncState exposed as sealed class StateFlow from ViewModel
✅ CRDT explained as the real-time collaboration upgrade path
Conclusion
Designing a notes app is a deceptively rich Android interview question. It's not about CRUD — it's about building a system that feels instant, never loses data, handles network absence gracefully, and reconciles state across devices in a way that makes sense to users.
The candidates who impress interviewers at Google, Microsoft, and Notion aren't the ones who describe a simple Room + Retrofit setup. They're the ones who name the offline-first principle explicitly, explain what syncStatus buys you, articulate the three conflict resolution strategies and their trade-offs, know why delta sync matters, and can describe what happens in the edit-on-two-devices-while-offline scenario without blinking.
The design pillars:
Room as single source of truth — UI never talks to the network; all reads and writes go through Room
Client-generated UUIDs — offline note creation requires a device-side identity
syncStatus on every record — the engine that drives the entire sync pipeline
WorkManager with unique work policy — the only tool that guarantees sync survives process kills
Delta sync with DataStore timestamp — don't re-download everything on every sync
Auto-merge with fallback to user resolution — the right conflict strategy for a notes app
Room FTS4 for search — orders of magnitude faster than LIKE at any meaningful note count
Frequently Asked Questions
What is offline-first architecture in an Android notes app?
Offline-first means the app reads and writes to a local database — not a server — as its primary source of truth.
Every note operation (create, edit, delete) goes straight to Room without touching the network. The UI reflects changes instantly. Sync to the server happens in the background, invisible to the user.
Key properties of an offline-first notes app:
All core features work with zero network connectivity
UI never shows a loading spinner waiting for the network
Data is never lost because the device lost signal mid-edit
Server sync runs silently via WorkManager when connectivity returns
Why use Room as the single source of truth?
Room is the single source of truth because it guarantees the UI always has data — regardless of network state.
If the ViewModel fetched data from Retrofit directly, a network failure would leave the screen blank. With Room:
The UI observes Room via Flow and reacts to every local change
The network only writes into Room — it never drives the UI
Cached data from the last session is available immediately on app open
Offline and online states are identical from the UI's perspective
How does WorkManager handle background sync in a notes app?
WorkManager is Android's recommended API for guaranteed background work that must complete even after a process kill or device reboot.
How it handles notes sync:
A NoteSyncWorker (CoroutineWorker) is enqueued with ExistingWorkPolicy.KEEP
WorkManager persists the job to its own Room-backed database internally
The job runs when the NetworkType.CONNECTED constraint is satisfied
If the process is killed mid-sync, WorkManager re-runs the worker on next launch
The KEEP policy is essential — without it, every app open could enqueue a fresh sync job, eventually queuing hundreds of redundant workers.
What is delta sync and why does it matter for a notes app?
Delta sync means uploading and downloading only what has changed since the last successful sync — not the entire note list every time.
How it works:
The client stores lastSyncTimestamp in DataStore (not SharedPreferences — it's thread-safe)
On each sync, the client sends this timestamp to the server
The server returns only notes created or modified after that timestamp
The client applies those changes to Room
Why it matters:
A user with 500 notes who edits 5 per day transfers ~25 KB, not ~2.5 MB
Sync is faster, uses less battery, and completes before the user notices
What is the syncStatus field and why does every note need one?
syncStatus is an enum on every NoteEntity that tracks whether the local state has been confirmed by the server.
The three states:
SYNCED — local and server agree; no action needed
PENDING — local change exists that hasn't been uploaded yet
CONFLICTED — a conflict was detected that needs resolution
Why it's necessary:
The sync worker queries WHERE syncStatus = PENDING to know what to upload
The UI shows a badge on CONFLICTED notes for user attention
Without this field, the app has no reliable way to track unsent edits
On process kill mid-sync, PENDING notes are safely retried on next launch
What are the conflict resolution strategies for a syncing notes app?
A conflict occurs when the same note is edited on two devices while both are offline. There are three standard strategies:
1. Last-Write-Wins (LWW)
Compares updatedAt timestamps; the newer version wins
Pro: simple, zero user friction
Con: silently discards edits the user cares about
Best for: short notes where losing an edit is low-cost
2. User Resolution
Marks the note CONFLICTED, stores both versions, asks the user to choose
Pro: zero data loss
Con: adds friction; requires a diff/merge UI
Best for: apps where every word matters (legal, medical notes)
3. Auto-Merge with Fallback
Uses a three-way merge to combine changes touching different text ranges
Falls back to user resolution only when edits genuinely overlap
Pro: handles most real-world conflicts silently
Con: more complex to implement
Best for: general-purpose notes apps (Google Keep, Apple Notes style)
Recommended approach: auto-merge with a user-resolution fallback. Most conflicts are two edits to different paragraphs — they merge automatically. Genuine overlapping edits are surfaced explicitly.
How does Room FTS4 full-text search work in a notes app?
Room FTS4 creates a SQLite virtual table that tokenises note content at write time and enables instant keyword searches using an inverted index.
How it differs from a LIKE query:
LIKE '%keyword%' scans every row sequentially — O(n)
FTS4 MATCH 'keyword' is a direct index lookup — O(log n)
Speed difference is imperceptible at 50 notes; significant at 5,000+
Setup in Room:
Annotate a virtual entity with @Fts4(contentEntity = NoteEntity::class)
Index title and contentPlainText (plain text stripped from rich text JSON)
Query with SELECT * FROM notes INNER JOIN notes_fts WHERE notes_fts MATCH :query
Append * to the query string to enable prefix matching
How do you store rich text in an Android notes app?
Rich text is serialised to JSON and stored in a contentJson TEXT column in Room. A separate contentPlainText column stores the stripped text for FTS indexing.
For a Google Keep–style app with basic formatting, JSON spans integrating with Android's native SpannableString is the cleaner choice. For a Notion-style app with blocks and embeds, a block-based document model (similar to ProseMirror) is more appropriate.
Why are client-generated UUIDs important for offline note creation?
Client-generated UUIDs allow notes to have a stable identity from the moment of creation — before any server interaction.
If the primary key were a server-assigned integer:
The app cannot create a note offline (no ID until the server responds)
The UI must wait for a network round-trip before displaying the new note
Process kill during creation leaves the app in an ambiguous state
With UUID.randomUUID() (Version 4):
The note has an ID instantly — used as the Room PK and the server's document ID
Offline creation, editing, and deletion all work without network
The UUID serves as an idempotency key for sync — if a sync POST is retried, the server rejects the duplicate
Collision probability: ~1 in 10²⁷ with a billion notes. Effectively zero.
Which companies ask the Android notes app system design question?
Google, Microsoft, Notion, Evernote, and Dropbox all ask variants of this question for senior Android engineer roles.
Why it's popular:
It's self-contained — clear product scope, no ambiguity about what to design
It touches every hard Android topic: Room schema, WorkManager, offline-first, conflict resolution
The depth scales cleanly — a junior candidate describes CRUD; a senior candidate explains syncStatus, delta sync, and three-way merge
It maps directly to real products these companies build and maintain
Red flags interviewers watch for:
Designing around Retrofit without mentioning offline handling
Not explaining what syncStatus buys you
Proposing SharedPreferences for lastSyncTimestamp (not thread-safe)
Missing soft delete — hard-deleting before sync drops data silently
The notes app question rewards candidates who understand that mobile system design is fundamentally about managing two sources of truth — the device and the server — and making the reconciliation between them invisible to the user. If you want to practice talking through conflict resolution strategies, delta sync, and WorkManager lifecycle decisions under real interview pressure, Mockingly.ai has Android-focused system design simulations specifically built for engineers preparing for senior roles at Google, Microsoft, Notion, and Evernote.
You've read the guide — now put your knowledge to the test. Our AI interviewer will challenge you with follow-up questions and give you real-time feedback on your system design.