Faculytics Docs

Caching

Redis-backed caching layer with namespace-aware key tracking and targeted invalidation.

This document describes the caching architecture used in api.faculytics, including the cache service abstraction, namespace-based invalidation, and integration points.

1. Overview

The caching layer provides a thin abstraction (CacheService) over NestJS CacheModule with namespace-aware key tracking for targeted invalidation. It uses Redis via @keyv/redisREDIS_URL is required (also used by BullMQ for job queues).

2. Technology Stack

  • Cache Framework: @nestjs/cache-manager v3 + cache-manager v7
  • Redis Adapter: @keyv/redis (Keyv-compatible adapter required by cache-manager v7)
  • Note: REDIS_URL is required — Redis is shared with BullMQ job queues

3. Architecture

CacheService (src/modules/common/cache/cache.service.ts)

A wrapper around CACHE_MANAGER that adds namespace-aware key tracking and logging.

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Controller  │────▶│   Service    │────▶│ CacheService │
│              │     │              │     │              │
│              │     │  wrap()      │     │  get/wrap    │──▶ Redis / Memory
│              │     │              │     │  invalidate  │
└──────────────┘     └──────────────┘     └──────────────┘

Key design decisions:

  • wrap(namespace, suffix, fn, ttlMs) — Checks cache first (logs HIT/MISS), delegates to cache.wrap() on miss for atomic get-or-set with coalescing
  • invalidateNamespace(namespace) — Deletes all tracked keys for a namespace via cache.del(), then clears the tracking set
  • In-memory keyRegistry: Map<CacheNamespace, Set<string>> — Lightweight, bounded (small number of cached endpoints), works identically for both Redis and in-memory stores
  • On app restart, the registry is empty but stale keys simply expire via TTL — no correctness issue

CacheNamespace (src/modules/common/cache/cache-namespaces.ts)

enum CacheNamespace {
  QUESTIONNAIRE_TYPES = 'q-types',
  QUESTIONNAIRE_VERSIONS = 'q-versions',
  ENROLLMENTS_ME = 'enrollments-me',
}

4. What is Cached

EndpointCache Key PatternTTLRationale
GET /enrollments/meenrollments-me:{userId}:{page}:{limit}30 minRead-heavy, per-user. Only mutated by hourly EnrollmentSyncJob.
GET /questionnaires/typesq-types:all1 hourRarely changes (admin-only operations).
GET /questionnaires/types/:type/versionsq-versions:{type}1 hourOnly changes on version CRUD.

Not cached: health (trivial), moodle endpoints (external proxy), chatkit (real-time streaming), drafts (frequently mutated per-user), auth/me (user loaded by guard every request).

5. Cache Invalidation

NamespaceInvalidated ByLocation
ENROLLMENTS_MEEnrollmentSyncJob after successful syncsrc/crons/jobs/enrollment-jobs/enrollment-sync.job.ts
QUESTIONNAIRE_TYPEScreateQuestionnaire(), PublishVersion(), DeprecateVersion()src/modules/questionnaires/services/questionnaire.service.ts
QUESTIONNAIRE_VERSIONSCreateVersion(), PublishVersion(), DeprecateVersion()src/modules/questionnaires/services/questionnaire.service.ts

6. Configuration

Environment VariableDefaultDescription
REDIS_URL(required)Redis connection URL. Required for both caching and job queues.
REDIS_KEY_PREFIXfaculytics:Namespace prefix for Redis keys.
REDIS_CACHE_TTL60Default TTL in seconds (applied when no per-key TTL is specified).

7. Observability

The CacheService logs at LOG level for all cache operations:

  • Cache HIT for key "enrollments-me:abc:1:10" — Served from cache
  • Cache MISS for key "enrollments-me:abc:1:10" — Fetched from database and cached
  • Invalidated 3 key(s) in namespace "enrollments-me" — Keys cleared after data mutation

8. Adding a New Cached Endpoint

  1. Add a new value to CacheNamespace in cache-namespaces.ts
  2. Wrap the service method: this.cacheService.wrap(NAMESPACE, suffix, () => fetchFn(), ttlMs)
  3. Add invalidation calls after mutations: this.cacheService.invalidateNamespace(NAMESPACE)
  4. Add CacheService mock to any affected test files