Prompt and scope
This frontend system-design question combines state management and distributed updates. The answer should connect local data, a mutation queue, server versions, and visible recovery states instead of simply adding a cache.
What the interviewer is testing
- Separating local drafts, pending mutations, and server-confirmed state.
- Choosing a conflict strategy and identifying fields that cannot be auto-merged.
- Handling retries, duplicate submissions, closed browsers, and multiple tabs.
- Making offline, syncing, conflict, and failure states understandable and recoverable.
Clarifying questions to ask
Ask whether fields are independent and whether any are audited or high-risk, such as money. Clarify attachments, server version or operation-log support, whether last-write-wins is acceptable, browser support, and local retention requirements.
The 30-second answer
I would use IndexedDB as a durable local store and record every edit with a clientMutationId and base server version. The UI shows the local result immediately and marks it pending. The app or a service worker sends the queue when online. The server replies with success, conflict, or validation failure using version checks and idempotency. Safe fields can merge automatically; high-risk fields show a diff for explicit choice. Every state supports retry or undo.
Step-by-step deep dive
1. Define the state model first
Each draft has serverVersion, localVersion, syncState, and lastError. Each pending mutation stores clientMutationId, field changes, creation time, and base version. Reads come from local storage; network responses merge only after version checks, so an older response cannot overwrite a newer edit.
2. Persist recoverable facts in IndexedDB
IndexedDB fits larger structured offline data, but the design must handle schema upgrades, quota errors, and browser eviction. Store drafts, mutation records, and conflicts separately, and commit a draft plus its queue record in one transaction. Minimize sensitive fields and clear them on logout or expiry.
3. Design sync triggers and idempotency
Try immediately while online and enqueue while offline. Where Background Sync is supported, register a unique tag for the service worker, but do not treat the API as a universal delivery guarantee. Every request carries clientMutationId and a base version; repeating the same id returns the same outcome without applying the mutation twice.
4. Match conflict policy to field risk
Independent fields such as labels can merge by field version. Money, permissions, and compliance statements should not silently use last-write-wins. On a conflict, show local value, server value, and edit times side by side; the user’s choice creates a new mutation. Automatic merges must record their inputs.
5. Handle failures, retries, and multiple contexts
Use exponential backoff while preserving queue order for network failures. Validation failures become editable error states rather than infinite retries. Multiple tabs can coordinate through BroadcastChannel or version checks so an old mutation is not sent twice. A durable queue resumes on the next launch, and the UI shows pending count and the last error.
A strong sample answer
I would define field risk and offline boundaries first. The client stores drafts and a mutation queue in IndexedDB; each mutation has a clientMutationId and base serverVersion, while the UI immediately shows the local result as pending. The app or service worker sends the queue after reconnecting, and the server returns success, conflict, or validation error using version checks and idempotency. Independent fields can merge automatically, but high-risk fields show a local-versus-server diff and require confirmation. Network failures back off; business failures become editable. Multiple tabs use version notifications so stale data cannot overwrite newer work. The interface clearly distinguishes offline, syncing, conflict, failure, and saved states.
Common mistakes
- Caching only the latest form without a mutation queue or base version.
- Applying timestamps or last-write-wins to every field.
- Treating Background Sync as a guarantee in every browser.
- Retrying without an idempotency key and creating duplicates.
- Logging conflicts only to the console with no user recovery path.
- Ignoring schema upgrades, quota, multiple tabs, and browser shutdown.
Follow-up questions and responses
What if two tabs edit at the same time?
Each tab subscribes to version changes and asks for a merge when its base is stale. Recheck the version before sending; the server still deduplicates the shared mutation id.
Can offline data leak?
Store only necessary fields locally, encrypt sensitive content, shorten retention, and clear it on logout, shared-device use, or policy change.
What if the browser lacks Background Sync?
Use app launch, focus, connectivity changes, and short polling as fallback triggers, and tell the user that records remain pending. Correctness must not depend on this API.
When is automatic merge safe?
Only when field semantics are independent, the version base is known, and temporary divergence is acceptable. Money, permissions, inventory, and audit fields should require explicit confirmation.