Keyed-cache schematic — responses routed to their query keys, with a stale response structurally unable to land
The structural fix in one picture: responses can only populate the cache entry their query key owns. The stale response has nowhere to land.

The planted bug was the easy part

The filter didn't filter because the frontend sent ?cat= while the backend read category, and FastAPI silently ignores unknown query params, so the WHERE clause never ran. The tempting fix is to add a cat alias server-side. But the backend was correct; aliasing it would add dead surface to a working API to accommodate a client typo. The fix was a one-line rename in the client, backend untouched, with evidence captured in both directions before and after. Smallest diff that addresses the actual cause.

The bug nobody asked about

Reading the fetch code on principle, not because a ticket said to, turned up the real problem. The library fetch ran in a useEffect keyed on the category, with no stale-response guard:

useEffect(() => {
  fetch(`/uops?category=${category}`)
    .then(r => r.json())
    .then(setUops);        // applied unconditionally
}, [category]);

Each category change fires a new request; responses aren't guaranteed to return in order; and setUops applies whichever lands last. A slow response for the previous category can overwrite the current one: the grid shows a category the user already left, and stays wrong until the next interaction. The loading flag cleared when the first promise settled, so the spinner disappeared while a request was still in flight.

There are four hand-rolled fixes, and the useful exercise is tabulating which axis each one actually addresses: an ignore-stale flag and an AbortController fix response ordering; debouncing and a keyed cache reduce request count. They're orthogonal and they compose, but each is a patch on a design in which the race is still conceptually possible.

"Keys results to the query" is a materially different argument than "React Query is nice." One patches the symptom; the other chooses a design in which the symptom cannot exist.

Migrating to TanStack Query made the stale render structurally impossible: results are keyed to ['uops','list',category], so a response can only ever populate the cache entry it belongs to. One dependency bought ordering, cancellation, in-flight dedup, a keyed cache, and loading/error state. The cost, a provider and a library "more than the minimum for a 17-row prototype," got logged honestly in the decision record.

Cache coherence across routes

The second feature, "mark as reviewed" on a detail page reflected back on the library card, is a cache-coherence problem in miniature. With a 60-second stale time, mutating on the detail page and navigating back shows the cached list with stale state: the cache defeats the exact behavior the feature demonstrates.

The optimistic mutation triad handles it, but the load-bearing detail is easy to miss: onMutate cancels in-flight queries (so a racing read can't clobber the optimistic write), snapshots the detail cache and every list bucket, and patches reviewed: true into all of them. Rollback restores every snapshot; settlement invalidates the namespace to reconcile against server truth. Patching only the detail cache, the obvious implementation, leaves the list stale and the bug half-fixed.

From UoPDetail.tsx, comments included

// Optimistic review: patch the detail cache AND every cached list
// bucket so the library card reflects the reviewed state instantly on
// return. Roll back on error; invalidate on settle to reconcile.
const { mutate, isPending } = useMutation({
  mutationFn: () => reviewUop(id),
  onMutate: async () => {
    await queryClient.cancelQueries({ queryKey: ['uops'] })
    const prevDetail = queryClient.getQueryData<UoP>(['uops', 'detail', id])
    const prevLists  = queryClient.getQueriesData<UoP[]>({ queryKey: ['uops', 'list'] })
    const patch = (u: UoP): UoP =>
      u.id === id ? { ...u, reviewed: true, reviewed_at: u.reviewed_at ?? reviewedAt } : u

    queryClient.setQueryData<UoP>(['uops', 'detail', id], (old) => (old ? patch(old) : old))
    queryClient.setQueriesData<UoP[]>({ queryKey: ['uops', 'list'] }, (old) =>
      Array.isArray(old) ? old.map(patch) : old,
    )
    return { prevDetail, prevLists }
  },
  onError: (_err, _vars, ctx) => {
    if (ctx?.prevDetail) queryClient.setQueryData(['uops', 'detail', id], ctx.prevDetail)
    ctx?.prevLists?.forEach(([key, data]) => queryClient.setQueryData(key, data))
  },
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['uops'] }),
})

The 2-second latency report that wasn't a query problem

A latency complaint came with an implied fix attached: paginate. Instrumenting first told a different story: the query was a single indexed SELECT over 17 rows, measuring 0.6–7ms warm. The slow path was the dev server's cold start (worker fork, imports, DDL, seeding ≈ 1s) plus a browser IPv6-to-IPv4 connection fallback: reproduced once at 1011ms cold, sub-10ms every request after.

Decision: no backend change, because cursor pagination doesn't fix a cold start. But the investigation surfaced a genuine adjacent gap, an endpoint returning an unbounded list with no server cap, which got logged as its own properly-scoped item, since an API contract change deserves review, not a drive-by "fix" justified by an unrelated symptom.

The habit: measure, find the real cause, decline the plausible fix, and separately log the real gap the plausible fix would have papered over.

Ranking dollars against head-counts

The design problem hiding in the data: 12 of 17 records carry a dollar impact, 5 carry an FTE or governance count, 2 have no number at all. The ledger view sorts by impact. A normalized score would rank a $1 row against a 9,999-FTE row on a scale that doesn't exist; it would invent precision. Instead, a pure tiered key: dollar rows rank first by amount, count rows next by count, unquantified rows sink last, with a stable ID tiebreaker. Deterministic, money-first, and readable top-to-bottom by a reviewer who has to trust it, which was the actual requirement. Unit-tested, because it's a pure function, which is exactly where a time-boxed testing budget buys the most: the tricky pure logic gets Vitest coverage; browser-verified behavior is labeled as browser-verified, not silently untested.

Small decisions that carry the quality

The remaining calls are individually small; the pattern is what matters. Filter, view, and sort state live in the URL: shareable, refresh-proof, and the substrate the next feature layered onto with zero new state machinery. The sorted list is computed in render, never stored: derived state stays derived. The impact-formatting logic is shared between card and detail page because it must not drift; the JSX stays per-surface because presentation legitimately differs. That is DRY applied at the right altitude. And the visual thesis of the whole grid rests on one specification detail: fixed card height with the stat region bottom-anchored, so impact figures align in a scannable column regardless of headline length.

Accessibility went in while it was cheap: a 4.5:1 contrast floor everywhere (including darkening a muted-text token so an 11px kicker survives a projector), a real <table> with aria-sort for the ledger, and reviewed-state signaled by text plus color, never color alone.

Learnings

The proactive sweep is the differentiator. The planted bug was assigned; the fetch race, the silently-forked SQLite file (a CWD-relative path creating two databases depending on launch directory), and the cold-start latency story were all found by reading code nobody flagged. Three bugs found by looking; one was assigned.

Structural fixes beat guards. The through-line from this project to everything I've built since: prefer the design where the failure is impossible over the design where it's checked for. A keyed cache instead of a stale flag. A grant row instead of a validated parameter. The guard can be forgotten; the structure can't.

Document what you deliberately didn't do. The final write-up lists the patterns consciously skipped (virtualization, offline, auth) as not applicable at this scale, so omissions read as decisions rather than gaps. Then it audits its own work harder than a reviewer would: 20 logged decisions and a line-by-line self-audit cataloguing 134 findings across every file, most of them documented trade-offs. That's the standard I hold client work to: not "it works," but "every decision has a defense in writing."