Server-Side Caching Strategy
Server-Side Caching Strategy
Introduced in v1.0.79 (PERF-18)
BlockManOS uses a three-layer caching architecture to minimise redundant database queries while ensuring data accuracy after mutations.
Overview
| Layer | Mechanism | TTL | Scope |
|---|---|---|---|
| Client-side | React Query staleTime: 30s | 30 seconds | Per browser session |
| Server-side | Next.js unstable_cache | 5 minutes | Shared across all users/requests |
| Request-level | React cache() | Single request | Per server render |
The server-side layer is managed by src/lib/server-cache.ts. All caching utilities in this module are intended for use in server-only contexts (tRPC procedures, React Server Components, route handlers).
Cached Functions
getCachedArrearsData(orgId, developmentId, includePartiallyPaid)
Returns arrears report rows for the given organisation (and optional development), drawn from the Next.js data cache when available.
Cache tags:
arrears:<orgId>— invalidated on any arrears mutation for the orgarrears:<orgId>:<developmentId>— invalidated on mutations scoped to a specific development
TTL: 5 minutes (300s)
Underlying query: A 4-table JOIN across service_charge_demands, units, owners, and developments, filtered by demand status and ordered by days overdue descending.
// tRPC procedure (reports.ts)
const rows = await getCachedArrearsData(
ctx.orgId,
input.developmentId ?? null,
input.includePartiallyPaid
);
getCachedCollectionData(orgId, developmentId, periodYear)
Returns collection report rows (one per service charge budget), drawn from the Next.js data cache when available.
Cache tags:
collection:<orgId>collection:<orgId>:<developmentId>
TTL: 5 minutes (300s)
Underlying query: Fetches service_charge_budgets joined to developments, with 6 correlated subqueries per budget row to compute demand counts, totals issued, collected, and outstanding. These subqueries are the primary performance concern at scale; caching prevents them from running on every tab switch.
// tRPC procedure (reports.ts)
const budgets = await getCachedCollectionData(
ctx.orgId,
input.developmentId ?? null,
input.periodYear ?? null
);
getCachedOrgById(id)
A React cache() wrapper around the organisation lookup. Unlike unstable_cache, this does not persist across requests — it deduplicates calls within a single server-rendering pass.
Use case: Multiple Server Components in the same page tree that independently need org data will each call this function, but only the first call hits the database; subsequent calls within the same request return the memoised result.
TTL: Single request lifetime only.
revalidateReportsCache(orgId, developmentId?)
Purges all arrears and collection cache entries for the given org (and optionally a specific development) from the Next.js data cache.
Call this from any tRPC mutation that modifies payment or demand state to ensure users see up-to-date data immediately after the action, rather than waiting for the 5-minute TTL to expire.
// Example: after recording a payment
await recordPayment(input);
revalidateReportsCache(ctx.orgId, input.developmentId);
This invalidates the following tags:
arrears:<orgId>collection:<orgId>arrears:<orgId>:<developmentId>(if developmentId provided)collection:<orgId>:<developmentId>(if developmentId provided)
Cache Tag Helpers
Two named helper functions are exported to generate consistent tag strings:
arrearsTag(orgId: string, developmentId?: string | null): string
collectionTag(orgId: string, developmentId?: string | null): string
Use these if you need to reference tags directly (e.g. when adding new cached functions in the future).
TTL Constants
| Constant | Value | Intended use |
|---|---|---|
ARREARS_TTL | 300 (5 min) | Arrears report cache |
COLLECTION_TTL | 300 (5 min) | Collection report cache |
REFERENCE_DATA_TTL | 3600 (1 hour) | Static reference data (VAT rates, LPT bands) |
REFERENCE_DATA_TTL is exported for use when caching reference/lookup data in future modules.
Health Endpoint Caching
The /api/health route is explicitly excluded from all caching:
Cache-Control: no-store, no-cache, must-revalidate
This ensures that load balancers, proxies, and platform health checks always receive a live response. The middleware also bypasses authentication for all paths that begin with /api/health (including trailing-slash variants).
Adding New Cached Queries
- Define the raw async fetch function (prefixed
_fetch…) insideserver-cache.ts. - Wrap it with
unstable_cache, providing:- A unique cache key array
- Appropriate tags using
arrearsTag()/collectionTag()or a new helper - A
revalidatevalue in seconds
- Export the wrapped function.
- If the cached data can be mutated, add the relevant
revalidateTag()calls torevalidateReportsCache()or a new invalidation helper. - Replace the raw DB call in the tRPC router with the cached function.