Caching Strategies
Caching Strategies
Section titled “Caching Strategies”This guide covers caching strategies for Tabula Lens applications, including application-level caching, database caching, and CDN caching to improve performance and reduce database load.
Overview
Section titled “Overview”Caching is one of the most effective ways to improve Tabula Lens application performance. By storing frequently accessed data in fast storage, you can:
- Reduce database load and query time
- Improve API response times
- Decrease network latency
- Enhance user experience
- Scale applications more efficiently
Caching Layers
Section titled “Caching Layers”Tabula Lens applications can benefit from multiple caching layers:
1. Application-Level Caching
- In-memory caching (Node.js)
- Distributed caching (Redis, Memcached)
- HTTP caching headers
2. Database Caching
- PostgreSQL query cache
- Materialized views
- Database connection pooling
3. CDN Caching
- Static asset caching
- API response caching
- Edge caching
Application-Level Caching
Section titled “Application-Level Caching”In-Memory Caching
Section titled “In-Memory Caching”Use Node.js in-memory caching for simple use cases:
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300, // 5 minutes default TTL checkperiod: 60, // Check for expired keys every 60s useClones: false // Performance optimization});
app.get('/api/tabula-lens', async (req, res) => { const cacheKey = `tabula:${JSON.stringify(req.query)}`; const cached = cache.get(cacheKey);
if (cached) { return res.json(cached); }
const result = await tabulaLens.query(req.query); cache.set(cacheKey, result); res.json(result);});Pros:
- Simple to implement
- No external dependencies
- Fast access times
Cons:
- Limited to single instance
- Data lost on restart
- Not suitable for distributed systems
Redis Caching
Section titled “Redis Caching”Use Redis for distributed caching:
const Redis = require('ioredis');const redis = new Redis(process.env.REDIS_URL);
app.get('/api/tabula-lens', async (req, res) => { const cacheKey = `tabula:${JSON.stringify(req.query)}`; const cached = await redis.get(cacheKey);
if (cached) { return res.json(JSON.parse(cached)); }
const result = await tabulaLens.query(req.query); await redis.setex(cacheKey, 300, JSON.stringify(result)); // 5 minutes res.json(result);});Advanced Redis Patterns:
// Cache with tags for invalidationasync function cacheWithTags(key, value, tags, ttl) { await redis.setex(key, ttl, JSON.stringify(value));
for (const tag of tags) { await redis.sadd(`tag:${tag}`, key); }}
async function invalidateByTag(tag) { const keys = await redis.smembers(`tag:${tag}`); if (keys.length > 0) { await redis.del(...keys); await redis.del(`tag:${tag}`); }}
// Usageawait cacheWithTags('users:1', userData, ['users', 'user:1'], 300);await invalidateByTag('users'); // Invalidate all user-related cachesPros:
- Distributed caching
- Persistent storage
- Advanced data structures
- High performance
Cons:
- External dependency
- Network latency
- Operational complexity
Cache-Aside Pattern
Section titled “Cache-Aside Pattern”Implement cache-aside pattern for better control:
class CacheAside { constructor(redis, tabulaLens) { this.redis = redis; this.tabulaLens = tabulaLens; }
async get(key, queryFn, ttl = 300) { // Try cache first const cached = await this.redis.get(key); if (cached) { return JSON.parse(cached); }
// Cache miss - fetch from database const data = await queryFn();
// Store in cache await this.redis.setex(key, ttl, JSON.stringify(data));
return data; }
async invalidate(key) { await this.redis.del(key); }
async invalidatePattern(pattern) { const keys = await this.redis.keys(pattern); if (keys.length > 0) { await this.redis.del(...keys); } }}
// Usageconst cache = new CacheAside(redis, tabulaLens);
app.get('/api/tabula-lens', async (req, res) => { const cacheKey = `tabula:${JSON.stringify(req.query)}`;
const result = await cache.get(cacheKey, async () => { return await tabulaLens.query(req.query); });
res.json(result);});Database Caching
Section titled “Database Caching”PostgreSQL Query Cache
Section titled “PostgreSQL Query Cache”PostgreSQL has built-in query caching:
-- Enable query cache (PostgreSQL configuration)shared_buffers = 256MBeffective_cache_size = 1GB
-- Monitor cache hit ratioSELECT sum(heap_blks_read) as heap_read, sum(heap_blks_hit) as heap_hit, sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as cache_hit_ratioFROM pg_statio_user_tables;Target cache hit ratio: > 95%
Materialized Views
Section titled “Materialized Views”Use materialized views for complex queries:
-- Create materialized viewCREATE MATERIALIZED VIEW user_order_summary ASSELECT u.id as user_id, u.name, u.email, COUNT(o.id) as order_count, SUM(o.total) as total_spentFROM users uLEFT JOIN orders o ON u.id = o.user_idGROUP BY u.id, u.name, u.email;
-- Refresh materialized viewREFRESH MATERIALIZED VIEW user_order_summary;
-- Query materialized view (much faster)SELECT * FROM user_order_summary WHERE user_id = 1;Automated Refresh:
-- Create function to refresh materialized viewCREATE OR REPLACE FUNCTION refresh_user_order_summary()RETURNS void AS $$BEGIN REFRESH MATERIALIZED VIEW CONCURRENTLY user_order_summary;END;$$ LANGUAGE plpgsql;
-- Schedule refresh (using pg_cron extension)SELECT cron.schedule('refresh-user-summary', '*/5 * * * *', 'SELECT refresh_user_order_summary()');Connection Pooling
Section titled “Connection Pooling”Use connection pooling to cache database connections:
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20, // Maximum pool size min: 5, // Minimum pool size idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000,});
const tabulaLens = new TabulaLens(pool);Benefits:
- Reuses database connections
- Reduces connection overhead
- Improves query performance
- Handles connection spikes
HTTP Caching
Section titled “HTTP Caching”Cache-Control Headers
Section titled “Cache-Control Headers”Implement HTTP caching headers:
app.get('/api/tabula-lens', async (req, res) => { const result = await tabulaLens.query(req.query);
// Set cache headers res.set('Cache-Control', 'public, max-age=300'); // 5 minutes res.set('ETag', generateETag(result));
// Check if client has cached version if (req.fresh) { return res.status(304).end(); }
res.json(result);});
function generateETag(data) { return crypto .createHash('md5') .update(JSON.stringify(data)) .digest('hex');}Conditional Requests
Section titled “Conditional Requests”Implement conditional requests with ETags:
app.get('/api/tabula-lens', async (req, res) => { const result = await tabulaLens.query(req.query); const etag = generateETag(result);
res.set('ETag', etag);
if (req.headers['if-none-match'] === etag) { return res.status(304).end(); }
res.json(result);});CDN Caching
Section titled “CDN Caching”Configure CDN caching for API responses:
// Set CDN cache headersapp.get('/api/tabula-lens', async (req, res) => { const result = await tabulaLens.query(req.query);
// CDN cache for 5 minutes, browser cache for 1 minute res.set('Cache-Control', 'public, s-maxage=300, max-age=60'); res.set('CDN-Cache-Control', 'public, max-age=300');
res.json(result);});Caching Strategies
Section titled “Caching Strategies”Strategy 1: Time-Based Expiration
Section titled “Strategy 1: Time-Based Expiration”Cache data for a fixed time period:
const cache = new NodeCache({ stdTTL: 300 }); // 5 minutes
app.get('/api/tabula-lens', async (req, res) => { const cacheKey = `tabula:${JSON.stringify(req.query)}`; const cached = cache.get(cacheKey);
if (cached) { return res.json(cached); }
const result = await tabulaLens.query(req.query); cache.set(cacheKey, result); res.json(result);});Use Cases:
- Data that changes infrequently
- Reference data
- Dashboard metrics
TTL Guidelines:
- Static data: 1 hour - 1 day
- Slowly changing data: 5 - 30 minutes
- Fast changing data: 1 - 5 minutes
- Real-time data: No caching
Strategy 2: Event-Based Invalidation
Section titled “Strategy 2: Event-Based Invalidation”Invalidate cache when data changes:
// Invalidate cache on data changesapp.post('/api/users', async (req, res) => { const result = await createUser(req.body);
// Invalidate related caches await cache.invalidatePattern('tabula:users:*'); await cache.invalidatePattern('tabula:user:*');
res.json(result);});
app.put('/api/users/:id', async (req, res) => { const result = await updateUser(req.params.id, req.body);
// Invalidate specific user cache await cache.invalidate(`tabula:user:${req.params.id}`); await cache.invalidatePattern('tabula:users:*');
res.json(result);});Use Cases:
- User-generated content
- Frequently updated data
- Data consistency requirements
Strategy 3: Write-Through Cache
Section titled “Strategy 3: Write-Through Cache”Write data to cache and database simultaneously:
class WriteThroughCache { constructor(redis, tabulaLens) { this.redis = redis; this.tabulaLens = tabulaLens; }
async write(key, value, queryFn, ttl = 300) { // Write to database await queryFn();
// Write to cache await this.redis.setex(key, ttl, JSON.stringify(value)); }
async read(key, queryFn, ttl = 300) { // Try cache first const cached = await this.redis.get(key); if (cached) { return JSON.parse(cached); }
// Cache miss - fetch from database const data = await queryFn();
// Store in cache await this.redis.setex(key, ttl, JSON.stringify(data));
return data; }}Use Cases:
- Read-heavy workloads
- Data that must be consistent
- Low latency requirements
Strategy 4: Write-Behind Cache
Section titled “Strategy 4: Write-Behind Cache”Write to cache immediately, database asynchronously:
class WriteBehindCache { constructor(redis, tabulaLens) { this.redis = redis; this.tabulaLens = tabulaLens; this.queue = []; }
async write(key, value, queryFn) { // Write to cache immediately await this.redis.set(key, JSON.stringify(value));
// Queue database write this.queue.push({ key, queryFn });
// Process queue asynchronously this.processQueue(); }
async processQueue() { while (this.queue.length > 0) { const { key, queryFn } = this.queue.shift(); try { await queryFn(); } catch (error) { // Re-queue on failure this.queue.unshift({ key, queryFn }); } } }}Use Cases:
- High write throughput
- Temporary data inconsistency acceptable
- Performance-critical writes
Strategy 5: Multi-Level Caching
Section titled “Strategy 5: Multi-Level Caching”Implement multiple cache levels:
class MultiLevelCache { constructor(l1Cache, l2Cache, tabulaLens) { this.l1Cache = l1Cache; // In-memory (fast) this.l2Cache = l2Cache; // Redis (distributed) this.tabulaLens = tabulaLens; }
async get(key, queryFn, ttl = 300) { // Try L1 cache first const l1Cached = this.l1Cache.get(key); if (l1Cached) { return l1Cached; }
// Try L2 cache const l2Cached = await this.l2Cache.get(key); if (l2Cached) { const data = JSON.parse(l2Cached); this.l1Cache.set(key, data); // Populate L1 return data; }
// Cache miss - fetch from database const data = await queryFn();
// Store in both caches this.l1Cache.set(key, data); await this.l2Cache.setex(key, ttl, JSON.stringify(data));
return data; }
async invalidate(key) { this.l1Cache.del(key); await this.l2Cache.del(key); }}Use Cases:
- High-performance requirements
- Distributed systems
- Reducing database load
Cache Invalidation Strategies
Section titled “Cache Invalidation Strategies”Time-Based Invalidation
Section titled “Time-Based Invalidation”// Set TTL on cache entriesawait redis.setex(key, 300, value); // Expires in 5 minutesEvent-Based Invalidation
Section titled “Event-Based Invalidation”// Invalidate on data changesapp.post('/api/users', async (req, res) => { await createUser(req.body); await cache.invalidatePattern('users:*'); res.json({ success: true });});Manual Invalidation
Section titled “Manual Invalidation”// Admin endpoint to clear cacheapp.post('/admin/cache/clear', async (req, res) => { await cache.flushAll(); res.json({ success: true });});Tag-Based Invalidation
Section titled “Tag-Based Invalidation”// Cache with tagsawait cache.set('user:1', userData, ['users', 'user:1']);
// Invalidate by tagawait cache.invalidateByTag('users'); // Invalidates all user cachesCaching Best Practices
Section titled “Caching Best Practices”1. Cache Key Design
Section titled “1. Cache Key Design”// ✅ Good: Descriptive cache keysconst cacheKey = `user:${userId}:profile`;const cacheKey = `tabula:query:${JSON.stringify(query)}`;
// ❌ Avoid: Generic or unclear keysconst cacheKey = 'data';const cacheKey = 'query';2. Appropriate TTL
Section titled “2. Appropriate TTL”// ✅ Good: Context-appropriate TTLawait redis.setex('static:data', 3600, data); // 1 hourawait redis.setex('user:data', 300, data); // 5 minutesawait redis.setex('realtime:data', 10, data); // 10 seconds
// ❌ Avoid: One-size-fits-all TTLawait redis.setex(key, 3600, data); // Too long for dynamic data3. Cache Size Management
Section titled “3. Cache Size Management”// ✅ Good: Limit cache sizeconst cache = new NodeCache({ maxKeys: 1000, // Maximum number of keys checkperiod: 60});
// ❌ Avoid: Unbounded cache growthconst cache = new NodeCache(); // Can grow indefinitely4. Error Handling
Section titled “4. Error Handling”// ✅ Good: Graceful fallbacktry { const cached = await redis.get(key); if (cached) return JSON.parse(cached);} catch (error) { console.error('Cache error:', error); // Fall back to database}
const data = await queryFn();return data;
// ❌ Avoid: Cache failures breaking applicationconst cached = await redis.get(key); // Can crash if Redis is down5. Monitoring
Section titled “5. Monitoring”// Monitor cache performanceconst cacheStats = { hits: 0, misses: 0, hitRate: () => cacheStats.hits / (cacheStats.hits + cacheStats.misses)};
app.get('/api/tabula-lens', async (req, res) => { const cached = await redis.get(key); if (cached) { cacheStats.hits++; return res.json(JSON.parse(cached)); }
cacheStats.misses++; const data = await queryFn(); await redis.setex(key, ttl, JSON.stringify(data)); res.json(data);});Cache Monitoring
Section titled “Cache Monitoring”Key Metrics
Section titled “Key Metrics”- Cache Hit Ratio: Percentage of requests served from cache
- Cache Miss Ratio: Percentage of requests that miss cache
- Cache Latency: Time to read from cache
- Cache Size: Current cache memory usage
- Eviction Rate: Rate at which items are evicted
Monitoring Implementation
Section titled “Monitoring Implementation”// Redis monitoringasync function getCacheStats() { const info = await redis.info('stats'); const keyspace = await redis.info('keyspace');
return { hits: info.keyspace_hits, misses: info.keyspace_misses, hitRate: info.keyspace_hits / (info.keyspace_hits + info.keyspace_misses), keys: keyspace.db0.keys, };}
// Log cache stats periodicallysetInterval(async () => { const stats = await getCacheStats(); logger.info('Cache stats', stats);}, 60000); // Every minuteTroubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”Issue: Low cache hit ratio
Solution:
- Review cache key design
- Check TTL settings
- Analyze access patterns
- Consider cache warming
Issue: High memory usage
Solution:
- Implement cache size limits
- Use appropriate TTL
- Monitor cache eviction
- Consider data compression
Issue: Stale data
Solution:
- Implement event-based invalidation
- Reduce TTL for frequently changing data
- Use cache versioning
- Implement cache validation
Issue: Cache stampede
Solution:
// Implement cache lockingasync function getWithLock(key, queryFn, ttl = 300) { const cached = await redis.get(key); if (cached) return JSON.parse(cached);
// Acquire lock const lockKey = `lock:${key}`; const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10);
if (lock) { try { const data = await queryFn(); await redis.setex(key, ttl, JSON.stringify(data)); return data; } finally { await redis.del(lockKey); } } else { // Wait for lock to release await new Promise(resolve => setTimeout(resolve, 100)); return getWithLock(key, queryFn, ttl); }}Next Steps
Section titled “Next Steps”- Implement caching strategy appropriate for your use case
- Set up cache monitoring and alerting
- Regularly review cache performance metrics
- Check Performance Characteristics for performance benchmarks