Ir al contenido

ADR-023 — Frontend Data Invalidation and Freshness Management

ADR-023 — Frontend Data Invalidation and Freshness Management

Sección titulada «ADR-023 — Frontend Data Invalidation and Freshness Management»

Accepted

Coveris has multiple Angular pages (dashboard, organigram, coverage, roster, hours, structure) that consume the same Django REST API endpoints via independent httpResource() instances. When data is mutated on one page (e.g., creating an org unit in Structure), other pages display stale data until the user performs a hard refresh.

Angular 21’s httpResource() provides reactive data fetching, but each instance is autonomous — there is no built-in cross-component invalidation mechanism. The project prohibits RxJS for state management ([[adr-002-b-frontend-dev-stack|ADR-002-b]]), requires stateless computation ([[adr-011-weekly-only-compute|ADR-011]]), and mandates that coverage states are never cached ([[adr-016-capacity-planning-patterns|ADR-016]]). [[adr-020-coverage-dashboard-strategy|ADR-020]] left the refresh strategy as an open question.

Implement DataVersionService — a singleton Angular service (providedIn: 'root') that acts as a centralized invalidation bus using Angular signals.

Structure (core/services/data-version.service.ts, 27 lines):

  • 5 numeric signals, one per data domain: orgUnits, positions, employees, assignments, tags.
  • 1 computed aggregate: any() — sum of all 5 signals.
  • 1 method: bump(domain) — increments the specified domain signal.

Producers call dv.bump(domain) after successful HTTP writes:

SignalWhat it invalidatesWho bumps
orgUnitsOrg tree, nesting, unit namesOrgUnitsService
positionsPositions, required hoursPositionsService
employeesEmployee list, status, personal dataRosterDetail
assignmentsEmployee-position assignmentsAssignmentService
tagsEmployee tags and position tagsPositionsService, RosterDetail

Consumers create an effect() that reads the relevant domain signals and calls .reload() on their httpResource instances:

ConsumerDomains watchedRationale
CoverageServiceorgUnits, positions, employees, assignmentsCoverage aggregation depends on all four; tags don’t affect it
DashboardorgUnits, positions, employees, assignmentsDisplays coverage-summary only
Rosterpositions, employees, assignments, tagsEmployee list with tags; org structure irrelevant
RosterDetailemployees, assignments, tagsSingle-employee view; org/position changes don’t affect it
OrgChartDetailall 5 (any())Candidate assignment panel joins all domains
Horasemployees, assignments, tagsHours balance depends on contract tags + assignments

Rules:

  1. Every mutation service calls dv.bump(domain) after a successful HTTP write (POST/PATCH/DELETE).
  2. Every consumer creates an effect() that reads the relevant domain signals and calls .reload() on its resources.
  3. Each effect uses a let initialized = false closure flag to skip the first execution during construction.
  4. Component-level effects are automatically destroyed by Angular on route navigation (via DestroyRef). Only providedIn: 'root' services have always-alive effects.
  5. Never call dv.bump() inside an effect() — the reactive graph must be unidirectional.
  6. Never do manual .reload() and dv.bump() on the same resource in the same code path — choose one to avoid double-fetching.
  7. Consumers should watch only the domains that affect their displayed data, not any(), to minimize unnecessary HTTP requests.
  • All pages automatically reflect mutations without manual browser refresh.
  • The pattern is 100% signal-based, consistent with [[adr-002-b-frontend-dev-stack|ADR-002-b]] (no RxJS for state).
  • Supports [[adr-016-capacity-planning-patterns|ADR-016]] (coverage never cached) and [[adr-011-weekly-only-compute|ADR-011]] (stateless computation) — it invalidates fetches, it does not persist computed state.
  • Answers the open refresh strategy question from [[adr-020-coverage-dashboard-strategy|ADR-020]] (signal-driven invalidation).
  • CoverageService (root singleton) reloads on every relevant bump even if no one is viewing the coverage page. Acceptable for MVP scale.
  • If consumers use any() instead of specific domains, unnecessary HTTP requests are generated. Rule 7 mitigates this.