Skip to content

Caching

Redis-based caching system for improving performance by reducing database queries and speeding up frequently accessed data.

Overview

  • Redis for in-memory caching
  • Singleton pattern for connection management
  • JSON serialization for complex objects
  • TTL support for automatic expiration
  • Cache invalidation strategies

Redis Setup

Connection Configuration

// adapters/shared/cache/redis.ts
import { createClient, RedisClientType } from '@redis/client';

export class RedisCache implements AbstractCache {
  private static instance: RedisCache;
  private client: RedisClientType;

  static getInstance() {
    if (!RedisCache.instance) {
      RedisCache.instance = new RedisCache();
      const client = createClient({
        url: process.env.REDIS_URL,
      });

      client
        .on('error', (err) => logger.error(`Redis Client Error: ${err}`))
        .on('connect', () => logger.info('Redis connected'))
        .connect();

      RedisCache.instance.client = client;
    }
    return RedisCache.instance;
  }

  async get(key: string) {
    const value = await this.client.get(key);
    return value ? JSON.parse(value) : null;
  }

  async set(key: string, value: any, ttl?: number) {
    const stringValue = JSON.stringify(value);
    if (ttl) {
      await this.client.setEx(key, ttl, stringValue);
    } else {
      await this.client.set(key, stringValue);
    }
  }

  async delete(key: string) {
    await this.client.del(key);
  }

  async clear() {
    await this.client.flushAll();
  }
}

Environment Variables

# Redis
REDIS_URL=redis://localhost:6379
REDIS_PASSWORD=your-redis-password
REDIS_DB=0

# Cache TTL (in seconds)
CACHE_TTL_SHORT=300      # 5 minutes
CACHE_TTL_MEDIUM=1800    # 30 minutes
CACHE_TTL_LONG=3600      # 1 hour

Cache Interface

// app/shared/cache.ts
export abstract class AbstractCache {
  abstract get(key: string): Promise<any>;
  abstract set(key: string, value: any, ttl?: number): Promise<void>;
  abstract delete(key: string): Promise<void>;
  abstract clear(): Promise<void>;
}

Usage Patterns

Basic Caching

const cache = RedisCache.getInstance();

// Set value
await cache.set('user:123', userData);

// Get value
const user = await cache.get('user:123');

// Delete value
await cache.delete('user:123');

Cache with TTL

// Cache for 5 minutes
await cache.set('trending:services', services, 300);

// Cache for 1 hour
await cache.set('categories:all', categories, 3600);

Cache-Aside Pattern

async function getUser(userId: string) {
  const cacheKey = `user:${userId}`;

  // Try cache first
  let user = await cache.get(cacheKey);

  if (!user) {
    // Cache miss - fetch from database
    user = await userRepository.get(userId);

    // Store in cache
    await cache.set(cacheKey, user, 1800); // 30 min TTL
  }

  return user;
}

Cache Invalidation

// Invalidate on update
async function updateUser(userId: string, data: any) {
  const user = await userRepository.update(userId, data);

  // Invalidate cache
  await cache.delete(`user:${userId}`);

  return user;
}

// Invalidate related caches
async function updateService(serviceId: string, data: any) {
  const service = await serviceRepository.update(serviceId, data);

  // Invalidate multiple related caches
  await cache.delete(`service:${serviceId}`);
  await cache.delete(`services:owner:${service.ownerId}`);
  await cache.delete(`services:category:${service.categoryId}`);

  return service;
}

Caching Strategies

1. User Data

// Cache user profile
const cacheKey = `user:${userId}`;
await cache.set(cacheKey, user, 1800); // 30 minutes

// Cache user permissions
const permKey = `user:${userId}:permissions`;
await cache.set(permKey, permissions, 3600); // 1 hour

2. Service Catalog

// Cache active services list
const servicesKey = 'services:active';
await cache.set(servicesKey, services, 300); // 5 minutes

// Cache service details
const serviceKey = `service:${serviceId}`;
await cache.set(serviceKey, service, 1800); // 30 minutes

3. Categories

// Cache category tree (rarely changes)
const categoriesKey = 'categories:all';
await cache.set(categoriesKey, categories, 7200); // 2 hours

4. Trending/Popular Data

// Cache trending services (updates frequently)
const trendingKey = 'services:trending';
await cache.set(trendingKey, trending, 600); // 10 minutes

// Cache popular searches
const searchKey = `search:${query}`;
await cache.set(searchKey, results, 1800); // 30 minutes

5. Session Data

// Cache active sessions
const sessionKey = `session:${userId}`;
await cache.set(sessionKey, sessionData, 86400); // 24 hours

Cache Key Patterns

Naming Convention

// Entity-based keys
`user:${userId}`
`service:${serviceId}`
`order:${orderId}`

// Collection keys
`services:active`
`services:category:${categoryId}`
`services:owner:${ownerId}`

// Query result keys
`search:${query}`
`trending:services`
`popular:categories`

// Session keys
`session:${userId}`
`auth:token:${tokenHash}`

Key Namespace Organization

user:123                          # User entity
user:123:permissions              # User permissions
user:123:services                 # User's services
service:456                       # Service entity
service:456:options               # Service options
services:active                   # Active services list
services:category:789             # Services in category
search:logo design                # Search results
trending:services                 # Trending services
session:user:123                  # User session

Cache Warming

Pre-populate cache with frequently accessed data:

async function warmCache() {
  logger.info('Warming up cache...');

  // Cache active services
  const services = await serviceRepository.findAll({ status: 'active' });
  await cache.set('services:active', services, 300);

  // Cache categories
  const categories = await categoryRepository.findAll();
  await cache.set('categories:all', categories, 7200);

  // Cache trending services
  const trending = await serviceRepository.getTrending(10);
  await cache.set('services:trending', trending, 600);

  logger.info('Cache warming complete');
}

// Run on application startup
warmCache();

Cache Monitoring

Cache Hit Rate

let cacheHits = 0;
let cacheMisses = 0;

async function getCached(key: string, fetchFn: () => Promise<any>) {
  const cached = await cache.get(key);

  if (cached) {
    cacheHits++;
    return cached;
  }

  cacheMisses++;
  const data = await fetchFn();
  await cache.set(key, data);

  return data;
}

// Calculate hit rate
function getCacheHitRate() {
  const total = cacheHits + cacheMisses;
  return total > 0 ? (cacheHits / total) * 100 : 0;
}

Cache Size Monitoring

async function getCacheStats() {
  const info = await cache.client.info('memory');
  const keys = await cache.client.dbSize();

  return {
    memoryUsed: parseMemoryUsage(info),
    totalKeys: keys,
    hitRate: getCacheHitRate(),
  };
}

Best Practices

✅ Do

  • Use appropriate TTL – Short TTL for volatile data, long for stable data
  • Invalidate on updates – Clear cache when data changes
  • Use consistent key naming – Follow namespace convention
  • Handle cache failures – Fallback to database if cache unavailable
  • Monitor hit rates – Track cache effectiveness
  • Serialize consistently – Use JSON for complex objects
  • Set reasonable limits – Don't cache everything

❌ Don't

  • Don't cache user-specific data indefinitely – Use sessions or short TTL
  • Don't forget to invalidate – Stale cache is worse than no cache
  • Don't cache large objects – Redis has memory limits
  • Don't use cache as primary storage – It's volatile
  • Don't cache sensitive data – Or encrypt it
  • Don't over-complicate keys – Keep them simple and readable

Redis Configuration

redis.conf

# Memory
maxmemory 256mb
maxmemory-policy allkeys-lru  # Evict least recently used keys

# Persistence (optional)
save 900 1        # Save after 900 sec if at least 1 key changed
save 300 10       # Save after 300 sec if at least 10 keys changed
save 60 10000     # Save after 60 sec if at least 10000 keys changed

# Security
requirepass your-redis-password

# Network
bind 127.0.0.1
port 6379

# Logging
loglevel notice
logfile /var/log/redis/redis-server.log

Testing

Mock Cache

// tests/mocks/cache.mock.ts
export class MockCache implements AbstractCache {
  private store = new Map<string, any>();

  async get(key: string) {
    return this.store.get(key) || null;
  }

  async set(key: string, value: any) {
    this.store.set(key, value);
  }

  async delete(key: string) {
    this.store.delete(key);
  }

  async clear() {
    this.store.clear();
  }
}

Cache Tests

describe('Cache', () => {
  let cache: RedisCache;

  beforeAll(() => {
    cache = RedisCache.getInstance();
  });

  afterEach(async () => {
    await cache.clear();
  });

  it('should set and get value', async () => {
    await cache.set('test:key', { data: 'value' });
    const result = await cache.get('test:key');

    expect(result).toEqual({ data: 'value' });
  });

  it('should delete value', async () => {
    await cache.set('test:key', 'value');
    await cache.delete('test:key');
    const result = await cache.get('test:key');

    expect(result).toBeNull();
  });

  it('should expire with TTL', async () => {
    await cache.set('test:key', 'value', 1); // 1 second TTL
    await sleep(1500);
    const result = await cache.get('test:key');

    expect(result).toBeNull();
  });
});

Summary

The caching system provides:

  • Redis integration for fast in-memory storage
  • Singleton pattern for connection management
  • Flexible TTL for different data types
  • Cache invalidation strategies
  • Consistent key naming patterns
  • Performance monitoring capabilities
  • Fallback handling for reliability

This caching layer significantly improves application performance by reducing database load and speeding up frequently accessed data.