FSD Frontend System Design

Part VI β€” The Blueprint Β· Lesson 11.8

HLD – Email Client

In one lineAn offline-first local database that syncs both ways β€” the design is the sync protocol.

Where you've seen itTriaging your inbox offline, then watching every action apply correctly once you reconnect.

  • Time20 min
  • DiagramIndexedDB schema + two-way sync with a watermark
  • Chapter11 Β· High Level Design

Diagram

diagram
1–2 Β· SCOPE AND SCALE

  in scope   list Β· read Β· compose/send Β· search Β· labels Β· offline
  out        calendar Β· contacts Β· spam ML Β· attachments storage
  scale      50k messages per mailbox Β· offline is a core requirement
             multi-device: same account open in three places
diagram
3–4 Β· LOCAL-FIRST ARCHITECTURE

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  UI reads ONLY from IndexedDB (6.5)          β”‚  ← instant, always
  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚  messages   keyPath id Β· index by-thread,    β”‚
  β”‚                          by-date, by-label   β”‚
  β”‚  threads    id, messageIds[], lastMessageAt  β”‚
  β”‚  outbox     queued sends + pending actions   β”‚
  β”‚  meta       syncWatermark, deviceId          β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚  sync engine (service worker, 9.1)
                      β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  Mail API    β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  The UI never waits on the network. Ever.
diagram
6a Β· DEEP DIVE β€” TWO-WAY SYNC

  PULL                                PUSH
  GET /changes?since=<watermark>      replay outbox in order
       β”‚                                   β”‚
       β–Ό                                   β–Ό
  [{id, op:'update', labels:[...]},   POST /messages/9/archive
   {id, op:'delete'}, ...]                 β”‚
       β”‚                              β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
       β–Ό                            ack        conflict
  apply to IndexedDB                  β”‚            β”‚
  watermark = response.cursor    remove from   resolve (below)
                                   outbox

  Watermark = server-issued cursor, NOT a client clock β€”
  device clocks are wrong and unsynchronised.
diagram
6b Β· DEEP DIVE β€” CONFLICT RESOLUTION

  conflict type                     resolution
  ────────────────────────────────────────────────────────────
  label add/remove (commutative)    merge β€” apply both sides
  read/unread flag                  last-write-wins by server time
  delete vs edit                    delete wins, warn the user
  draft edited on two devices       keep both, surface "2 versions"
                                    ← never silently discard user text

  Rule: commutative ops merge; destructive ops need an explicit rule;
        user-authored content is never auto-discarded.

How it works

  1. Read path is local only β€” the UI queries IndexedDB, so the inbox opens instantly and works offline.
  2. Write path is optimistic: apply locally, enqueue in the outbox, replay on reconnect (9.1).
  3. Sync pulls a delta since the server watermark; full re-sync only on first login or corruption.
  4. Search runs locally over an IndexedDB index for cached mail, falling back to server search for older messages.
  5. Virtualize the message list (10.7) β€” 50,000 rows will not live in the DOM.

Trade-offs

ChoiceCost
Local-first readsStorage quota, eviction handling, migrations
Optimistic writesConflict resolution UI you must design
Local search indexBuild cost and staleness
Full sync on loginSlow first run; simple correctness

Interview angle

"The user archives 20 emails offline. What happens?"

  • Each action applies to IndexedDB immediately and appends to an ordered outbox; the UI reflects it at once.
  • On reconnect the outbox replays in order with idempotency keys, so retries can't double-apply.
  • Conflicts resolve by type β€” label changes merge, deletes win over edits, and conflicting drafts are kept as two versions rather than silently dropped.

Recap

  • The UI reads only from the local database; the network is a background concern.
  • Sync is a server-issued watermark plus an ordered outbox, never a client clock.
  • Classify conflicts: merge commutative ops, define rules for destructive ones, never discard user text.