Cache-aside pattern with Redis

Cache-aside is the simplest caching pattern. Your code checks Redis first. If the key exists, return the cached value. If not, query the database, store the result in Redis, and return. The cache sits alongside the database, which is exactly where aside comes from.

Read path

First call misses and hits the database. Every call for the next minute hits Redis instead.

after/services/cache.service.ts
typescript
import { Redis } from 'ioredis';

const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
const DEFAULT_TTL = 60;

let _redis: Redis | null = null;

export function getRedisClient(): Redis {
  if (!_redis) _redis = new Redis(REDIS_URL);
  return _redis;
}

export async function cacheGet<T>(key: string): Promise<T | null> {
  try {
    const cached = await getRedisClient().get(key);
    if (cached) return JSON.parse(cached) as T;
    return null;
  } catch {
    return null; // graceful degradation: act like a cache miss
  }
}

export async function cacheSet(
  key: string,
  value: unknown,
  ttlSeconds: number = DEFAULT_TTL,
): Promise<void> {
  try {
    await getRedisClient().set(key, JSON.stringify(value), 'EX', ttlSeconds);
  } catch {
    // swallow: cache miss on next read is the worst case
  }
}

Generic get, set, and delete wrappers around ioredis. Try/catch on every call for graceful degradation.

after/services/book.service.ts
typescript
import { db } from '../db';
import { books, Book } from '../db/schema';
import { cacheGet, cacheSet } from './cache.service';

const BOOKS_ALL_KEY = 'books:all';

export async function getAllBooks(): Promise<Book[]> {
  const cached = await cacheGet<Book[]>(BOOKS_ALL_KEY);
  if (cached) return cached;

  const fromDb = await db.select().from(books);
  await cacheSet(BOOKS_ALL_KEY, fromDb);
  return fromDb;
}

Same getAllBooks as before, now checking the cache first.

Read getAllBooks carefully. One line of cache check. If it returns something, we are done. If not, fall through to the database, store the result, and return. The rest of the service looks identical to the persistence phase. The cache is almost invisible to the caller, which is exactly what you want.

Quiz: Quiz

Loading practiceโ€ฆ