Skip to main content
All Docs
FeaturesBlockManOSUpdated March 26, 2026

PERF-18: Caching Strategy — Current State & Recommendations

PERF-18: Caching Strategy — Current State & Recommendations

Release: v1.0.80
Category: Performance · Data Patterns
Status: Audit complete — remediation pending

Overview

As part of a systematic performance audit of the Irish block management platform, PERF-18 examined the caching posture across the full data stack — from the tRPC QueryClient configuration on the client, through to server-side data fetching in Next.js.

The audit found that no caching layer is in place at any level. This means every user interaction that triggers a data fetch results in a full round-trip to the database, regardless of how expensive the underlying query is or how recently the same data was fetched.


Current State

Client-side: React Query / tRPC

The tRPC provider (src/lib/trpc/provider.tsx) initialises a QueryClient with default React Query settings. The critical implication is that the default staleTime is 0 ms — every cached query is considered stale immediately after it resolves. As a result, React Query refetches data on every component mount, even if the same query ran moments earlier.

// Current behaviour — staleTime defaults to 0ms
const queryClient = new QueryClient();

Server-side: No unstable_cache or React cache()

Neither of Next.js's server-side caching primitives is used anywhere in the codebase:

PrimitiveUsage
unstable_cache (Next.js)❌ Not used
cache() (React)❌ Not used

This means every server component render and every server action triggers a fresh database query.

High-Impact Queries

Two report queries are particularly expensive and are currently uncached:

QueryCost
agedDebtorsReportJoins 4 tables; groups results into 5 age buckets
arrearsReportComplex aggregation across service charge and payment records

Both are fetched fresh on every visit to the relevant report pages.

Reference Data

The Irish legislation reference data router is approximately 66 KB in size. This file is parsed and evaluated on every request with no memoisation, adding unnecessary overhead to any route that depends on it.


Why This Matters

For an OMC (Owners' Management Company) management platform, reports like aged debtors and arrears are frequently consulted by agents — sometimes multiple times per session. Without caching:

  • Database load scales linearly with page views, even for identical requests.
  • Report pages feel slow because every visit incurs full aggregation query costs.
  • Reference data (legislation lookups, etc.) is redundantly re-fetched despite being essentially static.

Recommendations

1. Set a global staleTime in QueryClient

Updating the QueryClient initialisation to use a sensible default staleTime will immediately reduce redundant client-side refetches for the vast majority of queries.

// src/lib/trpc/provider.tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30_000, // 30 seconds
    },
  },
});

This means data fetched within the last 30 seconds will be served from the React Query cache rather than triggering a new network request. Individual queries can override this default where fresher data is required.

2. Cache expensive aggregate reports with unstable_cache

For agedDebtorsReport and arrearsReport, wrap the underlying data fetcher in Next.js unstable_cache with a short revalidation window and a cache tag for targeted invalidation (e.g. when a payment is recorded).

import { unstable_cache } from 'next/cache';

const getCachedArrearsReport = unstable_cache(
  fetchArrearsData,
  ['arrears-report'],
  {
    revalidate: 300, // 5 minutes
    tags: ['arrears'],
  }
);

Using cache tags means that when a payment or adjustment is posted, you can call revalidateTag('arrears') to immediately invalidate stale report data without waiting for the TTL to expire.

3. Deduplicate within-request auth lookups with React cache()

Server components often call auth helper functions multiple times within a single render tree. React's cache() function deduplicates these calls so the underlying lookup runs only once per request.

import { cache } from 'react';

export const getCurrentUser = cache(async () => {
  // Auth lookup — runs at most once per server render
  return await getSessionUser();
});

This is particularly valuable for auth checks that are repeated across layout, page, and nested server components in the same route.


Affected File

  • src/lib/trpc/provider.tsxQueryClient initialisation with default (0 ms) staleTime

Next Steps

The three recommendations above are tracked for implementation in upcoming releases. Priority order:

  1. staleTime default (lowest risk, broadest impact)
  2. unstable_cache on agedDebtorsReport and arrearsReport
  3. React cache() on auth helpers