FSD Frontend System Design

Part III β€” The Speed Β· Lesson 6.5

Indexed DB

In one lineAn asynchronous, transactional, indexed store for hundreds of megabytes of structured data.

Where you've seen itGmail offline. Thousands of messages, searchable, on a plane.

  • Time16 min
  • DiagramObject store schema with indexes + async transaction flow
  • Chapter6 Β· Database & Caching

Diagram

diagram
STRUCTURE β€” a database, not a key-value bag

  Database "mail" (version 3)
  β”œβ”€β”€ Object store "messages"      keyPath: "id"
  β”‚   β”œβ”€β”€ index "by-thread"        on threadId
  β”‚   β”œβ”€β”€ index "by-date"          on receivedAt
  β”‚   └── index "by-unread"        on isUnread
  β”‚       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚       β”‚ {id:"m1", threadId:"t9", subject:…}  β”‚  structured objects,
  β”‚       β”‚ {id:"m2", threadId:"t9", …}          β”‚  not strings
  β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”œβ”€β”€ Object store "attachments"   Blobs, Files, ArrayBuffers
  └── Object store "outbox"        queued sends (see 9.1)
diagram
ASYNC + TRANSACTIONAL β€” never blocks the main thread

  main thread ──[render]──[render]──[render]──[render]──►
                    β”‚                    β–²
      db.put(msg) β”€β”€β”˜                    β”‚ resolves later
                    β–Ό                    β”‚
  IDB thread    [ transaction ── write β”€β”€β”˜ ]
                  atomic: all writes commit, or none do
diagram
VERSION UPGRADE β€” the one-time schema hook

  open('mail', 3)
      β”‚
      β”œβ”€ existing version 3?  ──► ready, no upgrade
      └─ version 2 on disk?   ──► onupgradeneeded fires
                                    create new stores/indexes
                                    migrate or delete old data
                                    ← the ONLY place schema changes

How it works

  1. Open the database with a version number; schema changes happen only inside onupgradeneeded.
  2. Define object stores with a keyPath and add indexes for every field you'll query by β€” without one you're scanning.
  3. All reads and writes run inside a transaction, which is atomic and auto-commits when the microtask queue drains.
  4. Store real objects β€” nested structures, Blobs, Files, ArrayBuffers β€” no serialisation needed.
  5. Use a wrapper. The raw API is event-based and verbose; idb (β‰ˆ1KB) makes it promise-based and readable.
js
import { openDB } from 'idb';

const db = await openDB('mail', 3, {
  upgrade(db, oldVersion) {
    if (oldVersion < 1) {
      const s = db.createObjectStore('messages', { keyPath: 'id' });
      s.createIndex('by-thread', 'threadId');
    }
    if (oldVersion < 3) db.createObjectStore('outbox', { keyPath: 'id' });
  },
});

await db.put('messages', message);
const thread = await db.getAllFromIndex('messages', 'by-thread', 't9');

Trade-offs

StrengthCost
GBs of quota, async, transactionalVerbose raw API
Indexed queries, cursors, rangesNo SQL, no joins β€” you model manually
Stores binary data directlySchema migrations you must write
Works in workers and service workersEvictable under storage pressure

Quota and eviction: browsers grant a share of free disk and may evict under pressure. Call navigator.storage.persist() for critical data, and always be able to re-fetch β€” IndexedDB is a cache, not a database of record.

Interview angle

"How do you make a mail client work offline?"

  • IndexedDB for messages with indexes on thread and date, plus an outbox store for queued sends.
  • Service worker (9.1) serves the shell and static assets; the data layer reads IDB first, then revalidates.
  • Sync is the real design: a cursor/updatedAt watermark, last-write-wins or explicit conflict UI, and replay of the outbox on reconnect.

Recap

  • Async, transactional, indexed, gigabytes β€” the only serious client-side store.
  • Schema changes live in onupgradeneeded and nowhere else.
  • Use a promise wrapper, index every query field, and treat it as evictable.