Skip to main content
All Docs
FeaturesBlockManOSUpdated March 26, 2026

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

LayerMechanismTTLScope
Client-sideReact Query staleTime: 30s30 secondsPer browser session
Server-sideNext.js unstable_cache5 minutesShared across all users/requests
Request-levelReact cache()Single requestPer 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 org
  • arrears:<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

ConstantValueIntended use
ARREARS_TTL300 (5 min)Arrears report cache
COLLECTION_TTL300 (5 min)Collection report cache
REFERENCE_DATA_TTL3600 (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

  1. Define the raw async fetch function (prefixed _fetch…) inside server-cache.ts.
  2. Wrap it with unstable_cache, providing:
    • A unique cache key array
    • Appropriate tags using arrearsTag() / collectionTag() or a new helper
    • A revalidate value in seconds
  3. Export the wrapped function.
  4. If the cached data can be mutated, add the relevant revalidateTag() calls to revalidateReportsCache() or a new invalidation helper.
  5. Replace the raw DB call in the tRPC router with the cached function.