Skip to content

ADR-001: Frontend Cache-Invalidation Mechanism

ADR-001: Frontend Cache-Invalidation Mechanism

Section titled “ADR-001: Frontend Cache-Invalidation Mechanism”

Author: Miguel Pinilla Date: 2026-06-02 Status: Accepted

The Arda SPA caches item and kanban-card data per browser tab through ItemCardsProvider (see Data Flow and Caching). The cache keeps reads cheap but introduces a per-tab view of state that diverges from the backend the moment another user or another tab writes. Two cases need an invalidation mechanism: a sibling tab in the same browser, and a different browser entirely (different user, different machine).

Today the backend has no push channel. The infrastructure to add one (SSE, WebSocket, or a managed pub/sub) is a multi-team commitment, requires the operations service to publish lifecycle events, and shifts our operational profile (long-lived connections through Amplify, per-tenant fan-out, reconnection handling). The decision below scopes the invalidation question to what can land in the SPA alone, with no backend change.

  • Time to ship. The signal is needed now; backend push is multi-quarter work.
  • No backend dependency. Solution must work with the existing REST surface.
  • Same-browser cross-tab correctness. A user with two tabs open is the most reported scenario; the loop must close in sub-second time for that case.
  • Cross-browser bounded latency. Two users on different browsers must converge within a documented SLA, not “eventually” and not “next page load”.
  • Operational kill switch. Whatever fires periodic traffic must be disablable per environment without redeploy.
  • Bounded cost on idle tabs. A user with a backgrounded items page must not produce continuous network traffic.
  • Description: operations publishes item-changed events on a per-tenant channel; SPA subscribes through a long-lived connection terminated at the BFF.
  • Pros: Lowest theoretical latency, zero idle polling.
  • Cons: Multi-team backend change; new operational surface (connection handling, reconnection, per-tenant fan-out, Amplify long-lived-connection support). Does not exist today and is months of work.
  • Description: SPA polls the backend on an interval for every visible item; signal arrives within one poll cycle.
  • Pros: Trivial to implement; works cross-browser and cross-tab uniformly.
  • Cons: Same-browser cross-tab case eats a full poll interval even though the signal is in-process; per-tab polling multiplies network traffic by tab count; no obvious “stop polling when idle” gate without extra plumbing.

Option C: Local invalidation bus only (BroadcastChannel)

Section titled “Option C: Local invalidation bus only (BroadcastChannel)”
  • Description: Producers publish on BroadcastChannel('arda-item-stale'); sibling tabs subscribe and refresh.
  • Pros: Solves the same-browser cross-tab case at sub-second latency with no backend change.
  • Cons: Does not solve the cross-browser case at all. Two users on different browsers never see each other’s writes.

Option D: Local bus plus bounded interval poll (this ADR)

Section titled “Option D: Local bus plus bounded interval poll (this ADR)”
  • Description: itemStaleBus over BroadcastChannel for same-browser cross-tab propagation; pollTimer on ItemCardsProvider for cross-browser propagation. Both feed the same cache through the same consumer. The poll is gated by visibility and by the presence of mounted subscribers.
  • Pros: Solves both cases. Sub-second latency for cross-tab; bounded latency for cross-browser. No backend change. The poll is gated so idle tabs cost nothing; the interval is env-tunable with a kill switch.
  • Cons: Two mechanisms instead of one. Cross-browser latency is bounded by the poll interval, not by an event channel. Adds periodic background traffic on tabs that are visible and have subscribers.

We chose Option D because it solves both the same-browser cross-tab and cross-browser cases with no backend dependency and bounds the periodic cost to tabs that are both visible and have an active subscriber. The primary factors were time to ship (Option A is months of work we cannot afford to wait for), the latency floor on Option B’s same-browser case (an event channel already exists in-browser; not using it would be wasteful), and the cross-browser gap in Option C.

The poll interval is set to 120 seconds by default — long enough that a visible items page with a few subscribers produces negligible aggregate load on the BFF, short enough that a second user sees a banner within a couple of minutes of a remote write. The interval is read from NEXT_PUBLIC_ITEM_CARDS_POLL_MS at module evaluation; non-positive values disable polling entirely, providing the operational kill switch.

  • Same-browser cross-tab correctness with sub-second latency and no backend cost.
  • Cross-browser correctness with a documented latency floor (one poll interval).
  • The cache layer below the signal is unchanged. The signal is purely additive.
  • The kill switch is real — flipping NEXT_PUBLIC_ITEM_CARDS_POLL_MS=0 disables the cross-process path without rebuilding behavior, leaving the cross-tab path operational.
  • Two mechanisms to reason about and to test, where one would have been simpler.
  • Cross-browser latency is the poll interval in the worst case. There is no faster signal for two users on different browsers without adopting Option A.
  • Periodic background traffic on visible-and-subscribed tabs. The gating bounds it; it does not eliminate it.
  • BroadcastChannel is universally available on the browsers Arda supports today. A capability fallback exists for environments where it is missing; those environments lose only the cross-tab path and continue to rely on the poll-driven cross-process path.
  • The mechanism is anchored to item-card data only. Other cached data sets (auth tokens, Redux-persisted UI state, order-queue) are unaffected and continue to use their existing freshness strategies.
  • Operational runbook entry for the kill switch: when to flip NEXT_PUBLIC_ITEM_CARDS_POLL_MS=0, expected effect, recovery procedure. Not yet written.
  • Revisit when backend push lands (if it does). At that point the poll becomes redundant and the bus’s consumer would shift from “refresh on tick” to “refresh on event”. The bus surface stays.
  • If the signal proves valuable for cache layers beyond item-cards, extract the bus into a reusable shape. Today the bus name and message envelope are item-cards-specific, which keeps the scope manageable.