Design and implement caching solutions
Designs and implements multi-layer caching solutions for browser, application, and database layers.
/plugin marketplace add davepoon/buildwithclaude/plugin install all-commands@buildwithclaudeDesign and implement caching solutions
Caching Strategy Analysis
Browser and Client-Side Caching
HTTP Cache Headers:
// Express.js middleware
app.use((req, res, next) => {
// Static assets with long-term caching
if (req.url.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) {
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1 year
res.setHeader('ETag', generateETag(req.url));
}
// API responses with short-term caching
if (req.url.startsWith('/api/')) {
res.setHeader('Cache-Control', 'public, max-age=300'); // 5 minutes
}
next();
});
Service Worker Caching:
// sw.js - Service Worker
const CACHE_NAME = 'app-cache-v1';
const urlsToCache = [
'/',
'/static/js/bundle.js',
'/static/css/main.css',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});
Application-Level Caching
Node.js Memory Cache:
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes default TTL
class CacheService {
static get(key) {
return cache.get(key);
}
static set(key, value, ttl = 600) {
return cache.set(key, value, ttl);
}
static del(key) {
return cache.del(key);
}
static flush() {
return cache.flushAll();
}
// Cache wrapper for expensive operations
static async memoize(key, fn, ttl = 600) {
let result = this.get(key);
if (result === undefined) {
result = await fn();
this.set(key, result, ttl);
}
return result;
}
}
// Usage example
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
const cacheKey = `user:${userId}`;
const user = await CacheService.memoize(
cacheKey,
() => getUserFromDatabase(userId),
900 // 15 minutes
);
res.json(user);
});
Redis Distributed Cache:
const redis = require('redis');
const client = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
});
class RedisCache {
static async get(key) {
try {
const value = await client.get(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error('Cache get error:', error);
return null;
}
}
static async set(key, value, ttl = 600) {
try {
const serialized = JSON.stringify(value);
if (ttl) {
await client.setex(key, ttl, serialized);
} else {
await client.set(key, serialized);
}
return true;
} catch (error) {
console.error('Cache set error:', error);
return false;
}
}
static async del(key) {
try {
await client.del(key);
return true;
} catch (error) {
console.error('Cache delete error:', error);
return false;
}
}
// Pattern-based cache invalidation
static async invalidatePattern(pattern) {
try {
const keys = await client.keys(pattern);
if (keys.length > 0) {
await client.del(keys);
}
return true;
} catch (error) {
console.error('Cache invalidation error:', error);
return false;
}
}
}
Database Query Caching
PostgreSQL Query Caching:
const { Pool } = require('pg');
const pool = new Pool();
class DatabaseCache {
static async cachedQuery(sql, params = [], ttl = 300) {
const cacheKey = `query:${Buffer.from(sql + JSON.stringify(params)).toString('base64')}`;
// Try cache first
let result = await RedisCache.get(cacheKey);
if (result) {
return result;
}
// Execute query and cache result
const dbResult = await pool.query(sql, params);
result = dbResult.rows;
await RedisCache.set(cacheKey, result, ttl);
return result;
}
// Invalidate cache by table
static async invalidateTable(tableName) {
await RedisCache.invalidatePattern(`query:*${tableName}*`);
}
}
// Usage
app.get('/api/products', async (req, res) => {
const products = await DatabaseCache.cachedQuery(
'SELECT * FROM products WHERE active = true ORDER BY created_at DESC',
[],
600 // 10 minutes
);
res.json(products);
});
MongoDB Caching with Mongoose:
const mongoose = require('mongoose');
// Mongoose query caching plugin
function cachePlugin(schema) {
schema.add({
cacheKey: { type: String, index: true },
cachedAt: { type: Date },
});
schema.methods.cache = function(ttl = 300) {
this.cacheKey = this.constructor.generateCacheKey(this);
this.cachedAt = new Date();
return this;
};
schema.statics.findCached = async function(query, ttl = 300) {
const cacheKey = this.generateCacheKey(query);
let result = await RedisCache.get(cacheKey);
if (result) {
return result;
}
result = await this.find(query);
await RedisCache.set(cacheKey, result, ttl);
return result;
};
schema.statics.generateCacheKey = function(data) {
return `${this.modelName}:${JSON.stringify(data)}`;
};
}
// Apply plugin to schema
const ProductSchema = new mongoose.Schema({
name: String,
price: Number,
category: String,
});
ProductSchema.plugin(cachePlugin);
API Response Caching
Express Cache Middleware:
function cacheMiddleware(ttl = 300) {
return async (req, res, next) => {
// Only cache GET requests
if (req.method !== 'GET') {
return next();
}
const cacheKey = `api:${req.originalUrl}`;
const cached = await RedisCache.get(cacheKey);
if (cached) {
return res.json(cached);
}
// Override res.json to cache the response
const originalJson = res.json;
res.json = function(data) {
RedisCache.set(cacheKey, data, ttl);
return originalJson.call(this, data);
};
next();
};
}
// Usage
app.get('/api/dashboard', cacheMiddleware(600), async (req, res) => {
const dashboardData = await getDashboardData();
res.json(dashboardData);
});
GraphQL Query Caching:
const { ApolloServer } = require('apollo-server-express');
const { ResponseCache } = require('apollo-server-plugin-response-cache');
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ResponseCache({
sessionId: (requestContext) =>
requestContext.request.http.headers.authorization || null,
maximumAge: 300, // 5 minutes default
scope: 'PUBLIC',
}),
],
cacheControl: {
defaultMaxAge: 300,
calculateHttpHeaders: false,
stripFormattedExtensions: false,
},
});
// Resolver-level caching
const resolvers = {
Query: {
products: async (parent, args, context) => {
return await DatabaseCache.cachedQuery(
'SELECT * FROM products WHERE category = $1',
[args.category],
600
);
},
},
};
Cache Invalidation Strategies
Event-Driven Cache Invalidation:
const EventEmitter = require('events');
const cacheInvalidator = new EventEmitter();
class CacheInvalidator {
static invalidateUser(userId) {
const patterns = [
`user:${userId}*`,
`api:/api/users/${userId}*`,
'api:/api/dashboard*', // If dashboard shows user data
];
patterns.forEach(async (pattern) => {
await RedisCache.invalidatePattern(pattern);
});
cacheInvalidator.emit('user:updated', userId);
}
static invalidateProduct(productId) {
const patterns = [
`product:${productId}*`,
'api:/api/products*',
'query:*products*',
];
patterns.forEach(async (pattern) => {
await RedisCache.invalidatePattern(pattern);
});
}
}
// Trigger invalidation on data changes
app.put('/api/users/:id', async (req, res) => {
const userId = req.params.id;
await updateUser(userId, req.body);
// Invalidate related caches
CacheInvalidator.invalidateUser(userId);
res.json({ success: true });
});
Frontend Caching Strategies
React Query Caching:
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 3,
refetchOnWindowFocus: false,
},
},
});
function ProductList() {
const { data: products, isLoading, error } = useQuery(
'products',
() => fetch('/api/products').then(res => res.json()),
{
staleTime: 10 * 60 * 1000, // 10 minutes
cacheTime: 30 * 60 * 1000, // 30 minutes
}
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}
Local Storage Caching:
class LocalStorageCache {
static set(key, value, ttl = 3600000) { // 1 hour default
const item = {
value,
expiry: Date.now() + ttl,
};
localStorage.setItem(key, JSON.stringify(item));
}
static get(key) {
const item = localStorage.getItem(key);
if (!item) return null;
const parsed = JSON.parse(item);
if (Date.now() > parsed.expiry) {
localStorage.removeItem(key);
return null;
}
return parsed.value;
}
static remove(key) {
localStorage.removeItem(key);
}
static clear() {
localStorage.clear();
}
}
Cache Monitoring and Analytics
Cache Metrics Collection:
class CacheMetrics {
static hits = 0;
static misses = 0;
static errors = 0;
static recordHit() {
this.hits++;
}
static recordMiss() {
this.misses++;
}
static recordError() {
this.errors++;
}
static getStats() {
const total = this.hits + this.misses;
return {
hits: this.hits,
misses: this.misses,
errors: this.errors,
hitRate: total > 0 ? (this.hits / total * 100).toFixed(2) : 0,
total,
};
}
static reset() {
this.hits = 0;
this.misses = 0;
this.errors = 0;
}
}
// Enhanced cache service with metrics
class MetricsCache {
static async get(key) {
try {
const value = await RedisCache.get(key);
if (value !== null) {
CacheMetrics.recordHit();
} else {
CacheMetrics.recordMiss();
}
return value;
} catch (error) {
CacheMetrics.recordError();
throw error;
}
}
}
// Metrics endpoint
app.get('/api/cache/stats', (req, res) => {
res.json(CacheMetrics.getStats());
});
Cache Warming and Preloading
Scheduled Cache Warming:
const cron = require('node-cron');
class CacheWarmer {
static async warmPopularData() {
console.log('Starting cache warming...');
// Warm popular products
const popularProducts = await DatabaseCache.cachedQuery(
'SELECT * FROM products ORDER BY view_count DESC LIMIT 100',
[],
3600 // 1 hour
);
// Warm user sessions
const activeUsers = await DatabaseCache.cachedQuery(
'SELECT id FROM users WHERE last_active > NOW() - INTERVAL 1 DAY',
[],
1800 // 30 minutes
);
console.log(`Warmed cache for ${popularProducts.length} products and ${activeUsers.length} users`);
}
static async warmOnDemand(cacheKeys) {
for (const key of cacheKeys) {
if (!(await RedisCache.get(key))) {
// Generate cache for missing keys
await this.generateCacheForKey(key);
}
}
}
}
// Schedule cache warming
cron.schedule('0 */6 * * *', () => { // Every 6 hours
CacheWarmer.warmPopularData();
});
Testing and Validation
Cache Testing:
// tests/cache.test.js
const request = require('supertest');
const app = require('../app');
describe('Cache Performance', () => {
test('should cache API responses', async () => {
// First request - should miss cache
const start1 = Date.now();
const response1 = await request(app).get('/api/products');
const duration1 = Date.now() - start1;
// Second request - should hit cache
const start2 = Date.now();
const response2 = await request(app).get('/api/products');
const duration2 = Date.now() - start2;
expect(response1.body).toEqual(response2.body);
expect(duration2).toBeLessThan(duration1 / 2); // Cached should be faster
});
test('should invalidate cache properly', async () => {
// Get initial data
const initial = await request(app).get('/api/products');
// Update data
await request(app)
.put('/api/products/1')
.send({ name: 'Updated Product' });
// Should get updated data
const updated = await request(app).get('/api/products');
expect(updated.body).not.toEqual(initial.body);
});
});