QuestionsSystem Design

Design a client data-fetching cache (SWR)

Client Data CacheHardSystem Design

Design a data-fetching layer like React Query / SWR. Apply RADIO; cover dedup, stale-while-revalidate, invalidation, and GC.

What it tests

Cache keying, request deduplication, background revalidation, optimistic mutation, and garbage collection.

Approach & answer

Requirements: a data layer that dedupes concurrent requests, serves cached data instantly, refreshes in the background, invalidates on mutation, and supports optimistic updates — essentially 'build your own React Query'. Architecture: a global cache keyed by a serialized query key; a useQuery(key, fetcher) hook reads the cache and subscribes to it; a coordinator ensures only ONE network request per key is in flight. Data model per key: { data, error, status, updatedAt, subscribers }. Interface: useQuery(key, fn), mutate(key), invalidate(key). Optimizations: STALE-WHILE-REVALIDATE — return cached (possibly stale) data immediately so the UI is instant, then refetch in the background and update subscribers when fresh data lands; DEDUPE — if three components mount with the same key, share ONE promise instead of three requests; INVALIDATE on mutation so dependent queries refetch; background refetch on window focus / reconnect / interval; OPTIMISTIC mutations — write the expected result into the cache, fire the request, roll back on error (the same pattern as optimistic UI). GARBAGE-COLLECT entries with no subscribers after a TTL so the cache doesn't grow unbounded. Why SWR beats fetch-in-useEffect: it removes duplicate requests, kills loading spinners on revisits (cache-first), and centralizes invalidation so the whole app stays consistent after a write. The subtle parts: a STABLE serialized key (sort object params), reference-stable reads so components don't re-render needlessly, and treating the cache as the single source of truth that components merely subscribe to.

Use this technique when

Building or reasoning about a data-fetching layer; explaining why React Query/SWR exist over raw useEffect fetches.

Complexity

Cache read O(1) by key; dedup makes N concurrent mounts cost 1 request. Memory bounded by GC of unsubscribed keys.

Code

const cache = new Map(); // key -> { data, promise, ts, subs:Set }

function useQuery(key, fetcher, { staleMs = 30000 } = {}) {
  const [, force] = React.useReducer((x) => x + 1, 0);
  let entry = cache.get(key);
  if (!entry) cache.set(key, (entry = { data: undefined, promise: null, ts: 0, subs: new Set() }));

  React.useEffect(() => {
    entry.subs.add(force);
    const stale = Date.now() - entry.ts > staleMs;
    if (stale && !entry.promise) {                  // dedup: one request per key
      entry.promise = fetcher(key).then((d) => {
        entry.data = d; entry.ts = Date.now(); entry.promise = null;
        entry.subs.forEach((fn) => fn());           // revalidate subscribers
      });
    }
    return () => entry.subs.delete(force);          // GC hook: drop key when subs empty
  }, [key]);

  return { data: entry.data };                       // stale-while-revalidate
}

References