Trovella Wiki

Cache-Aside Pattern

How to use cacheGet for cache-aside reads — API, TTL defaults, serialization, and usage examples.

How cacheGet Works

The cacheGet<T> function implements the cache-aside (lazy-loading) pattern:

  1. Check Redis for the given key.
  2. If the key exists, deserialize the JSON and return it.
  3. If the key is missing, call the fetcher function to compute the value.
  4. Serialize the result as JSON and store it in Redis with a TTL.
  5. Return the computed value.
import { cacheGet } from "@repo/cache";

const org = await cacheGet(
  `tenant:${orgId}:settings`,
  async () => {
    // This only runs on cache miss
    return db.query.organizations.findFirst({
      where: eq(organizations.id, orgId),
    });
  },
  600, // TTL in seconds (10 minutes)
);

Signature

function cacheGet<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttlSeconds?: number, // default: 300 (5 minutes)
): Promise<T>;

Parameters

ParameterTypeDefaultDescription
keystringThe cache key (see Key Conventions)
fetcher() => Promise<T>Async function that computes the value on miss
ttlSecondsnumber300Time-to-live in seconds

TTL Guidelines

Data typeSuggested TTLRationale
Tenant settings600s (10 min)Rarely changes, safe to serve slightly stale
Reference data (enums)3600s (1 hr)Almost never changes after deployment
User profile300s (5 min)Default — balances freshness with performance
Search results60-120sResults change as content is indexed
Rate-limit countersWindow sizeTTL matches the rate-limit window exactly

These are suggestions. Choose the shortest TTL that still provides meaningful cache hit rates for your use case.

Serialization

Values are stored as JSON via JSON.stringify / JSON.parse. This means:

  • Primitive types, plain objects, and arrays work out of the box.
  • Date objects are serialized as ISO strings — you must reconstruct them on read if needed.
  • Circular references will throw at write time.
  • undefined values in objects are silently dropped (standard JSON behavior).
  • Map, Set, BigInt, and class instances are not supported without custom serialization.

If you need to cache a value with non-JSON-safe types, transform it before caching:

const result = await cacheGet(
  `tenant:${orgId}:metrics:${period}`,
  async () => {
    const raw = await computeMetrics(orgId, period);
    // Dates are serialized as ISO strings — this is fine if consumers expect strings
    return raw;
  },
  300,
);

Error Behavior

  • If Redis is unreachable, getRedis() will attempt the connection (up to 3 retries). If all retries fail, the error propagates to the caller. The fetcher is never called.
  • If the fetcher throws, the error propagates to the caller. Nothing is written to the cache.
  • If the JSON parse fails on a cached value (data corruption), the error propagates. Consider wrapping cacheGet in a try/catch if you need fallback-to-fetcher behavior on corrupt cache entries.

There is no built-in stale-while-revalidate or circuit-breaker pattern. If Redis is down, requests that use cacheGet will fail unless you add your own error handling.

When Not to Use cacheGet

  • Write-heavy data that changes on every request — the cache hit rate will be near zero.
  • Data that must be real-time consistent — even a 5-second TTL introduces staleness.
  • Large payloads (> 1 MB) — Redis stores everything in memory; large values waste Upstash quota.
  • Sensitive data (tokens, passwords, PII) — unless you have a clear TTL and invalidation strategy. The Redis instance is shared across the application.

For these cases, query the database directly or use a different caching strategy.

On this page