Design and implement caching solutions
-
Caching Strategy Analysis
- Analyze application architecture and identify caching opportunities
- Assess current performance bottlenecks and data access patterns
- Define caching requirements (TTL, invalidation, consistency)
- Plan multi-layer caching architecture (browser, CDN, application, database)
- Evaluate caching technologies and storage solutions
-
Browser and Client-Side Caching
- Configure HTTP caching headers and cache policies:
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
- Implement in-memory and distributed 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
- Implement database-level caching strategies:
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
- Implement comprehensive API 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
- Implement intelligent cache invalidation:
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
- Implement client-side caching:
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
- Set up cache performance monitoring:
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
- Implement cache warming strategies:
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
- Set up cache 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); }); });