Hosted onhyper.mediavia theHypermedia Protocol

Query cache normalization: discussion summary and converging solutionSummary of the team discussion about normalized client cache, API shape, batching, invalidation, and perceived performance.

Context

The discussion started from a performance and maintainability concern: Seed clients currently receive and cache data that often includes embedded related resources, such as documents with author/profile data. That makes screens easy to render at first, but it also means the same resource can appear in many query results. When one embedded resource changes, the client has to either invalidate many queries or risk showing stale data.

The core question was not whether caching matters, but where the system should normalize data and how much the API should help.

Main positions in the conversation

One position argued that the current denormalized cache is becoming hard to reason about. If profile data appears inside many document/comment/search responses, a profile update can force broad invalidation and reloading. From this view, the client cache should be normalized so each resource has one canonical cached representation, and query results should point to those resources instead of duplicating them everywhere.

Another position agreed with normalized cache as a goal, but cautioned that normalization is much simpler if the API is shaped around object IDs and batched object fetches. Instead of returning deeply embedded related resources, endpoints can return IDs for related accounts, documents, comments, etc. The client can then coalesce those IDs and fetch the needed objects in batches. This may add some round trips, but it can also reduce repeated overfetching, improve HTTP/CDN cache behavior, and make both server and client code easier to reason about.

A concern was raised that removing embedded data could hurt performance, especially on web, by introducing waterfalls or UI flashes while related resources load. The counterpoint was that the server-side React Query loader can mitigate the first-load waterfall, and later loads may benefit from a warmer normalized/persistent cache. The team also noted that perceived performance matters: a small amount of overfetching may be acceptable if it avoids visible UI jumps, such as author names appearing late.

The discussion also touched on optimistic updates and cross-window consistency. Comments already use optimistic cache updates during mutations, but most relevant updates may arrive from outside the current window. The good news is that Seed already has invalidation messages between windows, so the missing piece is less “can windows communicate?” and more “what cache model should those invalidations update?”

What the team seems to be converging on

The team is converging on client-side data normalization, but with a pragmatic migration rather than a big-bang rewrite.

The likely direction is:

  1. Normalize important client resources into canonical per-object caches.

  2. Keep query results as lightweight references plus ordering/pagination state where possible.

  3. Prefer APIs that expose stable IDs for related resources and support batched/coalesced fetches.

  4. Avoid deeply nested embedded objects for hot paths where duplication causes invalidation and performance problems.

  5. Keep or selectively allow embedded data when it clearly improves perceived performance and avoids visible UI flicker.

  6. Use existing cross-window invalidation messages to update normalized resources consistently across windows.

  7. Measure the user-visible behavior, not only theoretical request counts.

In other words, the emerging solution is not “normalize everything blindly” and not “keep denormalized query blobs forever.” It is a normalized client cache with batched object loading, supported by API shapes that make related-resource fetching explicit, while preserving UX-critical fast paths when they are justified.

Design implications

A normalized cache should reduce broad query invalidation. For example, if a profile changes, the app should update the profile resource once and let all screens that reference that profile observe the update, instead of invalidating every query result that happened to embed that profile.

API responses should increasingly distinguish between resource identity and resource content. Lists can return ordered IDs and minimal display-critical fields; detail or batch endpoints can fill canonical resource records. This makes the cache easier to merge, update, persist, and share across windows.

Batching is important. If a page needs many authors or related documents, the client should aggregate IDs into sets and fetch them with coalesced/batched requests, avoiding both N+1 request storms and repeated nested overfetch.

Perceived performance should guide exceptions. If waiting for a related resource would cause layout jumps or missing labels, the system can either preload through the server-side loader, include a small stable preview field, or keep a carefully chosen embedded subset. The key is to treat embedding as a deliberate optimization, not the default cache model.

Open questions

  • Which resource types should be normalized first: profiles/accounts, documents, comments, citations, search results, or all high-churn resources?

  • What is the smallest API change that enables batched related-resource loading without breaking current screens?

  • Which embedded fields are genuinely needed for perceived performance, and which are just accidental overfetch?

  • How should normalized cache updates propagate between windows and persisted cache layers?

  • What metrics should decide success: fewer daemon requests, less bandwidth, fewer invalidations, faster perceived render, or simpler code?

Suggested next step

Start with one high-impact surface, such as comments or document lists with author profiles. Normalize the related account/profile records, keep list ordering in the query result, batch-load missing profiles, and use existing invalidation messages to update all windows. Then compare behavior before and after: request count, transferred bytes, visible loading/flicker, invalidation scope, and code complexity.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime