Skip to main content

Caching Strategies

Overview

Q01 Core APIs benefit from multi-layer caching to reduce latency and database load. Implement caching at client, CDN, application, and database levels for optimal performance.

Caching Layers:

  1. Browser Cache - Static assets, API responses
  2. CDN Cache - Media files, public content
  3. Application Cache - Redis/Memcached for API responses
  4. Database Cache - Query result cache

Benefits:

  • ✅ Reduced latency (faster response times)
  • ✅ Lower database load
  • ✅ Better scalability
  • ✅ Cost savings (fewer database queries)

Cache Invalidation

Event-Driven Invalidation

Use Outbox events to invalidate cache:

// cache-invalidator subscriber
class CacheInvalidator {
async handleEvent(event) {
const keys = this.getCacheKeys(event);

for (const key of keys) {
await redis.del(key);
console.log(`Invalidated: ${key}`);
}
}

getCacheKeys(event) {
const { AGGREGATE_TYPE, AGGREGATE_ID, PAYLOAD } = event;
const keys = [];

// Specific record
keys.push(`${AGGREGATE_TYPE}:${AGGREGATE_ID}`);

// List caches
keys.push(`${AGGREGATE_TYPE}:list:*`);

// Related caches
if (AGGREGATE_TYPE === 'PRD' && PAYLOAD.XPRD05) {
keys.push(`CAT:${PAYLOAD.XPRD05}:products`);
}

return keys;
}
}

Client-Side Caching

Browser Cache Headers

// CoreService sets cache headers
app.get('/api/v4/core/:dim/:id', (req, res) => {
// Public data - cache 5 minutes
res.set('Cache-Control', 'public, max-age=300');

// Private data - cache only in browser
res.set('Cache-Control', 'private, max-age=60');

// No cache for sensitive data
res.set('Cache-Control', 'no-store');
});

JavaScript Cache Implementation

class CachedAPIClient {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}

async get(url) {
const cached = this.cache.get(url);

if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}

const data = await fetch(url).then(r => r.json());

this.cache.set(url, {
data,
timestamp: Date.now()
});

return data;
}

invalidate(url) {
this.cache.delete(url);
}

clear() {
this.cache.clear();
}
}

Application-Level Caching

Redis Cache

const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

class APICache {
async get(key) {
const cached = await redis.get(key);
return cached ? JSON.parse(cached) : null;
}

async set(key, value, ttl = 300) {
await redis.set(key, JSON.stringify(value), 'EX', ttl);
}

async del(key) {
await redis.del(key);
}

async delPattern(pattern) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
}

// Usage
const cache = new APICache();

async function getProduct(id) {
const cacheKey = `product:${id}`;

// Check cache
let product = await cache.get(cacheKey);
if (product) return product;

// Fetch from API
product = await fetchProductFromAPI(id);

// Store in cache (5 minutes)
await cache.set(cacheKey, product, 300);

return product;
}

Cache-Aside Pattern

async function getProducts(filters) {
const cacheKey = `products:${JSON.stringify(filters)}`;

// 1. Check cache
let products = await cache.get(cacheKey);
if (products) {
console.log('Cache hit');
return products;
}

// 2. Cache miss - fetch from API
console.log('Cache miss');
products = await api.getProducts(filters);

// 3. Store in cache
await cache.set(cacheKey, products, 300);

return products;
}

CDN Caching

CloudFront Configuration

// Cache media files
app.get('/api/v4/media/:dim/:id/:field', (req, res) => {
// Long cache for immutable media
res.set('Cache-Control', 'public, max-age=31536000, immutable');
res.set('ETag', generateETag(file));

res.sendFile(filePath);
});

Cache Patterns

Read-Through Cache

class ReadThroughCache {
constructor(loader, ttl = 300) {
this.loader = loader;
this.cache = new Map();
this.ttl = ttl;
}

async get(key) {
const cached = this.cache.get(key);

if (cached && Date.now() - cached.timestamp < this.ttl * 1000) {
return cached.value;
}

// Load from source
const value = await this.loader(key);

this.cache.set(key, {
value,
timestamp: Date.now()
});

return value;
}
}

// Usage
const productCache = new ReadThroughCache(
async (id) => await api.getProduct(id),
300
);

const product = await productCache.get('123');

Write-Through Cache

async function updateProduct(id, data) {
// 1. Update database via API
await api.updateProduct(id, data);

// 2. Update cache immediately
await cache.set(`product:${id}`, data, 300);

return data;
}

Write-Behind Cache

class WriteBehindCache {
constructor() {
this.queue = [];
this.flushInterval = 5000; // 5 seconds

setInterval(() => this.flush(), this.flushInterval);
}

async set(key, value) {
// Update cache immediately
await cache.set(key, value);

// Queue for database write
this.queue.push({ key, value });
}

async flush() {
if (this.queue.length === 0) return;

const batch = this.queue.splice(0, 100);

for (const { key, value } of batch) {
await api.update(key, value);
}
}
}

Best Practices

✅ DO:

Set appropriate TTL:

// ✅ Good - different TTL for different data
await cache.set('static-config', data, 3600); // 1 hour
await cache.set('product-list', data, 300); // 5 minutes
await cache.set('user-session', data, 1800); // 30 minutes

Invalidate on writes:

// ✅ Good - invalidate related caches
async function updateProduct(id, data) {
await api.updateProduct(id, data);

await cache.del(`product:${id}`);
await cache.delPattern(`products:list:*`);
await cache.delPattern(`category:${data.category}:*`);
}

Use cache keys wisely:

// ✅ Good - structured keys
const key = `${tenant}:${dimension}:${id}:${version}`;

❌ DON'T:

Don't cache everything:

// ❌ Bad - caching frequently changing data
await cache.set('current-time', Date.now(), 3600);

// ✅ Good - cache stable data
await cache.set('product-catalog', products, 300);

Don't forget to invalidate:

// ❌ Bad - stale data
await api.updateProduct(id, data);
// Cache not invalidated!

// ✅ Good - invalidate immediately
await api.updateProduct(id, data);
await cache.del(`product:${id}`);

Summary

  • ✅ Multi-layer caching (browser, CDN, application, database)
  • ✅ Event-driven cache invalidation
  • ✅ Cache-aside pattern for reads
  • ✅ Write-through for immediate consistency
  • ✅ Appropriate TTL for different data types
  • ✅ Structured cache keys

Key Takeaways:

  1. Cache at multiple layers for best performance
  2. Invalidate cache on writes using Outbox events
  3. Set appropriate TTL based on data volatility
  4. Use Redis for application-level caching
  5. Implement cache patterns (read-through, write-through)
  6. Monitor cache hit rates