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
Context
Sección titulada «Context»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.
Decision
Sección titulada «Decision»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:
| Signal | What it invalidates | Who bumps |
|---|---|---|
orgUnits | Org tree, nesting, unit names | OrgUnitsService |
positions | Positions, required hours | PositionsService |
employees | Employee list, status, personal data | RosterDetail |
assignments | Employee-position assignments | AssignmentService |
tags | Employee tags and position tags | PositionsService, RosterDetail |
Consumers create an effect() that reads the relevant domain signals and calls .reload() on their httpResource instances:
| Consumer | Domains watched | Rationale |
|---|---|---|
| CoverageService | orgUnits, positions, employees, assignments | Coverage aggregation depends on all four; tags don’t affect it |
| Dashboard | orgUnits, positions, employees, assignments | Displays coverage-summary only |
| Roster | positions, employees, assignments, tags | Employee list with tags; org structure irrelevant |
| RosterDetail | employees, assignments, tags | Single-employee view; org/position changes don’t affect it |
| OrgChartDetail | all 5 (any()) | Candidate assignment panel joins all domains |
| Horas | employees, assignments, tags | Hours balance depends on contract tags + assignments |
Rules:
- Every mutation service calls
dv.bump(domain)after a successful HTTP write (POST/PATCH/DELETE). - Every consumer creates an
effect()that reads the relevant domain signals and calls.reload()on its resources. - Each effect uses a
let initialized = falseclosure flag to skip the first execution during construction. - Component-level effects are automatically destroyed by Angular on route navigation (via
DestroyRef). OnlyprovidedIn: 'root'services have always-alive effects. - Never call
dv.bump()inside aneffect()— the reactive graph must be unidirectional. - Never do manual
.reload()anddv.bump()on the same resource in the same code path — choose one to avoid double-fetching. - Consumers should watch only the domains that affect their displayed data, not
any(), to minimize unnecessary HTTP requests.
Consequences
Sección titulada «Consequences»- 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.