FSD Frontend System Design

Part III β€” The Speed Β· Lesson 6.9

API Caching

In one lineCache responses by key in the client, then decide the one hard thing: when they stop being true.

Where you've seen itPressing back to a product list and seeing it render instantly β€” populated, scrolled, no spinner.

  • Time16 min
  • DiagramCache key lifecycle + invalidation triggers
  • Chapter6 Β· Database & Caching

Diagram

diagram
CACHE KEY LIFECYCLE

  useQuery(['orders', {status:'open', page:2}])
             └────────── the KEY: endpoint + every parameter β”€β”€β”€β”€β”€β”˜
                                    β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β–Ό                           β–Ό                           β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  fresh          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  stale        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  FRESH  β”‚ ───────────────►│  STALE  β”‚ ─────────────►│ GARBAGE β”‚
   β”‚ serve,  β”‚  staleTime      β”‚ serve   β”‚  gcTime       β”‚ evicted β”‚
   β”‚ no fetchβ”‚  elapsed        β”‚ + refetchβ”‚  + unused     β”‚         β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  staleTime = how long the data is trusted    (0 by default)
  gcTime    = how long unused data is kept in memory
diagram
THE FOUR INVALIDATION TRIGGERS

  1. TIME        staleTime expires                    simplest
  2. MUTATION    user edits β†’ invalidate related keys  most important
  3. EVENT       WebSocket says entity changed (2.4)   most accurate
  4. FOCUS       tab regains focus / reconnects        cheap and effective

  Mutation-based invalidation is the one candidates forget.
diagram
OPTIMISTIC UPDATE β€” and the rollback that must exist

  user clicks "Like"
       β”‚
       β”œβ”€β–Ί write cache immediately        UI updates in 0ms
       β”‚
       β”œβ”€β–Ί POST /like ──► βœ“ 200  ──► reconcile with server response
       β”‚              └─► βœ— 500  ──► ROLL BACK to the snapshot
       β”‚                              + surface the failure
       └─ always snapshot before writing, or rollback is impossible

How it works

  1. Key by everything that changes the response β€” endpoint, filters, page, sort, and user/locale where relevant.
  2. Set staleTime per resource. A currency rate is stale in seconds; a country list is fresh for a day. The default of 0 refetches constantly.
  3. Invalidate on mutation: after a successful write, mark the affected keys stale so dependent screens refetch.
  4. Deduplicate in flight. Three components requesting the same key must produce one network request.
  5. Snapshot before optimistic writes, and always implement rollback β€” the failure path is the whole feature.
js
const { mutate } = useMutation({
  mutationFn: likePost,
  onMutate: async (id) => {
    await qc.cancelQueries({ queryKey: ['post', id] });
    const prev = qc.getQueryData(['post', id]);        // snapshot
    qc.setQueryData(['post', id], (p) => ({ ...p, liked: true }));
    return { prev };
  },
  onError: (_e, id, ctx) => qc.setQueryData(['post', id], ctx.prev), // rollback
  onSettled: (_d, _e, id) => qc.invalidateQueries({ queryKey: ['post', id] }),
});

Trade-offs

ApproachFreshnessComplexity
No cachePerfectSlow, spinner on every navigation
TTL onlyPredictably staleLow
Mutation invalidationCorrect after own writesMedium
Event-driven (WS)Correct after anyone's writesHigh
Optimistic + rollbackInstant, occasionally wrongHighest

Interview angle

"Two components need the same data. How many requests?"

  • One β€” the cache deduplicates by key and both components subscribe to the same entry.
  • That's precisely why the key must include every parameter; a missing filter serves the wrong cached data.
  • Layer invalidation: TTL as the floor, mutation invalidation for your own writes, and events for other users' writes.

Recap

  • The key is the contract: endpoint plus every parameter that changes the answer.
  • staleTime per resource, mutation invalidation for correctness, events for multi-user accuracy.
  • Optimistic updates without a snapshot and rollback are a bug, not a feature.