
Caching Strategy
- 499 installs
- 305 repo stars
- Updated March 4, 2026
- aj-geddes/useful-ai-prompts
caching-strategy is a skill that helps developers implement Redis, Memcached, CDN, and cache-invalidation patterns to reduce database load and improve API response latency.
About
caching-strategy is an aj-geddes/useful-ai-prompts skill for designing multi-layer caches that shrink database query volume and speed API responses under traffic spikes. The skill covers Redis and Memcached application caches, CDN edge caching for static and semi-static assets, TTL selection, and invalidation patterns that prevent stale reads after writes. Developers reach for caching-strategy when endpoints show repeated hot-key queries, p95 latency climbs, or origin databases saturate during peak load. Guidance includes quick-start patterns, reference architectures, and best practices for choosing what to cache, how long to keep it, and how to bust entries safely when upstream data changes.
- Supports Redis, Memcached, CDN, and distributed caching implementations
- Covers cache invalidation patterns, TTL management, and retry strategies
- Reduces database query load and improves API response times under high traffic
- Includes session storage, rate limiting, and expensive computation caching
- Provides ready-to-use TypeScript service class with connection handling
Caching Strategy by the numbers
- 499 all-time installs (skills.sh)
- Ranked #828 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aj-geddes/useful-ai-prompts --skill caching-strategyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 499 |
|---|---|
| repo stars | ★ 305 |
| Last updated | March 4, 2026 |
| Repository | aj-geddes/useful-ai-prompts ↗ |
How do you add Redis and CDN caching to APIs?
Add Redis, Memcached, and CDN caching layers that cut database load and speed up API responses.
Who is it for?
Backend developers optimizing high-read APIs where database queries or origin fetches dominate latency and infrastructure cost.
Skip if: Low-traffic internal tools with negligible query volume or systems where strong immediate consistency forbids any cached reads.
When should I use this skill?
API p95 latency rises, databases show hot-query saturation, or traffic spikes require edge and application caching layers.
What you get
Cache layer design, Redis or Memcached configuration patterns, CDN rules, TTL policies, and invalidation workflow documentation.
- cache architecture plan
- invalidation workflow
- redis or memcached configuration patterns
Files
Caching Strategy
Table of Contents
Overview
Implement effective caching strategies to improve application performance, reduce latency, and decrease load on backend systems.
When to Use
- Reducing database query load
- Improving API response times
- Handling high traffic loads
- Caching expensive computations
- Storing session data
- CDN integration for static assets
- Implementing distributed caching
- Rate limiting and throttling
Quick Start
Minimal working example:
import Redis from "ioredis";
interface CacheOptions {
ttl?: number; // Time to live in seconds
prefix?: string;
}
class CacheService {
private redis: Redis;
private defaultTTL = 3600; // 1 hour
constructor(redisUrl: string) {
this.redis = new Redis(redisUrl, {
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3,
});
this.redis.on("connect", () => {
console.log("Redis connected");
});
this.redis.on("error", (error) => {
// ... (see reference guides for full implementation)Reference Guides
Detailed implementations in the references/ directory:
| Guide | Contents |
|---|---|
| Redis Cache Implementation (Node.js) | Redis Cache Implementation (Node.js) |
| Cache Decorator (Python) | Cache Decorator (Python) |
| Multi-Level Cache | Multi-Level Cache |
| Cache Invalidation Strategies | Cache Invalidation Strategies |
| HTTP Caching Headers | HTTP Caching Headers |
Best Practices
✅ DO
- Set appropriate TTL values
- Implement cache warming for critical data
- Use cache-aside pattern for reads
- Monitor cache hit rates
- Implement graceful degradation on cache failure
- Use compression for large cached values
- Namespace cache keys properly
- Implement cache stampede prevention
- Use consistent hashing for distributed caching
- Monitor cache memory usage
❌ DON'T
- Cache everything indiscriminately
- Use caching as a fix for poor database design
- Store sensitive data without encryption
- Forget to handle cache misses
- Set TTL too long for frequently changing data
- Ignore cache invalidation strategies
- Cache without monitoring
- Store large objects without consideration
Cache Decorator (Python)
Cache Decorator (Python)
import functools
import json
import hashlib
from typing import Any, Callable, Optional
from redis import Redis
import time
class CacheDecorator:
def __init__(self, redis_client: Redis, ttl: int = 3600):
self.redis = redis_client
self.ttl = ttl
def cache_key(self, func: Callable, *args, **kwargs) -> str:
"""Generate cache key from function name and arguments."""
# Create deterministic key from function and arguments
key_parts = [
func.__module__,
func.__name__,
str(args),
str(sorted(kwargs.items()))
]
key_string = ':'.join(key_parts)
key_hash = hashlib.md5(key_string.encode()).hexdigest()
return f"cache:{func.__name__}:{key_hash}"
def __call__(self, func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Generate cache key
cache_key = self.cache_key(func, *args, **kwargs)
# Try to get from cache
cached = self.redis.get(cache_key)
if cached:
print(f"Cache HIT: {cache_key}")
return json.loads(cached)
# Cache miss - execute function
print(f"Cache MISS: {cache_key}")
result = func(*args, **kwargs)
# Store in cache
self.redis.setex(
cache_key,
self.ttl,
json.dumps(result)
)
return result
# Add cache invalidation method
def invalidate(*args, **kwargs):
cache_key = self.cache_key(func, *args, **kwargs)
self.redis.delete(cache_key)
wrapper.invalidate = invalidate
return wrapper
# Usage
redis = Redis(host='localhost', port=6379, db=0)
cache = CacheDecorator(redis, ttl=300)
@cache
def get_user_profile(user_id: int) -> dict:
"""Fetch user profile from database."""
print(f"Fetching user {user_id} from database...")
# Simulate database query
time.sleep(1)
return {
'id': user_id,
'name': 'John Doe',
'email': 'john@example.com'
}
# First call - cache miss
profile = get_user_profile(123) # Takes 1 second
# Second call - cache hit
profile = get_user_profile(123) # Instant
# Invalidate cache
get_user_profile.invalidate(123)Cache Invalidation Strategies
Cache Invalidation Strategies
class CacheInvalidation {
constructor(private cache: CacheService) {}
/**
* Time-based invalidation (TTL)
*/
async setWithTTL(key: string, value: any, seconds: number): Promise<void> {
await this.cache.set(key, value, { ttl: seconds });
}
/**
* Tag-based invalidation
*/
async setWithTags(key: string, value: any, tags: string[]): Promise<void> {
// Store value
await this.cache.set(key, value);
// Store tag associations
for (const tag of tags) {
await this.cache.redis.sadd(`tag:${tag}`, key);
}
}
async invalidateByTag(tag: string): Promise<number> {
// Get all keys with this tag
const keys = await this.cache.redis.smembers(`tag:${tag}`);
if (keys.length === 0) return 0;
// Delete all keys
await Promise.all(keys.map((key) => this.cache.delete(key)));
// Delete tag set
await this.cache.redis.del(`tag:${tag}`);
return keys.length;
}
/**
* Event-based invalidation
*/
async invalidateOnEvent(
entity: string,
id: string,
event: "create" | "update" | "delete",
): Promise<void> {
const patterns = [
`${entity}:${id}`,
`${entity}:${id}:*`,
`${entity}:list:*`,
`${entity}:count`,
];
for (const pattern of patterns) {
await this.cache.deletePattern(pattern);
}
}
/**
* Version-based invalidation
*/
async setVersioned(key: string, value: any, version: number): Promise<void> {
const versionedKey = `${key}:v${version}`;
await this.cache.set(versionedKey, value);
await this.cache.set(`${key}:version`, version);
}
async getVersioned(key: string): Promise<any> {
const version = await this.cache.get<number>(`${key}:version`);
if (!version) return null;
return await this.cache.get(`${key}:v${version}`);
}
}HTTP Caching Headers
HTTP Caching Headers
import express from "express";
const app = express();
// Cache-Control middleware
function cacheControl(
maxAge: number,
options: {
private?: boolean;
noStore?: boolean;
noCache?: boolean;
mustRevalidate?: boolean;
staleWhileRevalidate?: number;
} = {},
) {
return (
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
const directives: string[] = [];
if (options.noStore) {
directives.push("no-store");
} else if (options.noCache) {
directives.push("no-cache");
} else {
directives.push(options.private ? "private" : "public");
directives.push(`max-age=${maxAge}`);
if (options.staleWhileRevalidate) {
directives.push(
`stale-while-revalidate=${options.staleWhileRevalidate}`,
);
}
}
if (options.mustRevalidate) {
directives.push("must-revalidate");
}
res.setHeader("Cache-Control", directives.join(", "));
next();
};
}
// Static assets - long cache
app.use("/static", cacheControl(31536000), express.static("public"));
// API - short cache with revalidation
app.get(
"/api/data",
cacheControl(60, { staleWhileRevalidate: 300 }),
(req, res) => {
res.json({ data: "cached for 60s" });
},
);
// Dynamic content - no cache
app.get(
"/api/user/profile",
cacheControl(0, { private: true, noCache: true }),
(req, res) => {
res.json({ user: "always fresh" });
},
);
// ETag support
app.get("/api/resource/:id", async (req, res) => {
const resource = await getResource(req.params.id);
const etag = generateETag(resource);
res.setHeader("ETag", etag);
// Check if client has current version
if (req.headers["if-none-match"] === etag) {
return res.status(304).end();
}
res.json(resource);
});
function generateETag(data: any): string {
return require("crypto")
.createHash("md5")
.update(JSON.stringify(data))
.digest("hex");
}Multi-Level Cache
Multi-Level Cache
interface CacheLevel {
get(key: string): Promise<any>;
set(key: string, value: any, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
}
class MemoryCache implements CacheLevel {
private cache = new Map<string, { value: any; expiry: number }>();
async get(key: string): Promise<any> {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.value;
}
async set(key: string, value: any, ttl: number = 60): Promise<void> {
this.cache.set(key, {
value,
expiry: Date.now() + ttl * 1000,
});
}
async delete(key: string): Promise<void> {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
}
class RedisCache implements CacheLevel {
constructor(private redis: Redis) {}
async get(key: string): Promise<any> {
const value = await this.redis.get(key);
return value ? JSON.parse(value) : null;
}
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
await this.redis.setex(key, ttl, JSON.stringify(value));
}
async delete(key: string): Promise<void> {
await this.redis.del(key);
}
}
class MultiLevelCache {
private levels: CacheLevel[];
constructor(levels: CacheLevel[]) {
this.levels = levels; // Ordered from fastest to slowest
}
async get<T>(key: string): Promise<T | null> {
for (let i = 0; i < this.levels.length; i++) {
const value = await this.levels[i].get(key);
if (value !== null) {
// Backfill faster caches
for (let j = 0; j < i; j++) {
await this.levels[j].set(key, value);
}
return value as T;
}
}
return null;
}
async set(key: string, value: any, ttl?: number): Promise<void> {
// Set in all cache levels
await Promise.all(this.levels.map((level) => level.set(key, value, ttl)));
}
async delete(key: string): Promise<void> {
await Promise.all(this.levels.map((level) => level.delete(key)));
}
}
// Usage
const cache = new MultiLevelCache([new MemoryCache(), new RedisCache(redis)]);
// Get from fastest available cache
const data = await cache.get("user:123");
// Set in all caches
await cache.set("user:123", userData, 3600);Redis Cache Implementation (Node.js)
Redis Cache Implementation (Node.js)
import Redis from "ioredis";
interface CacheOptions {
ttl?: number; // Time to live in seconds
prefix?: string;
}
class CacheService {
private redis: Redis;
private defaultTTL = 3600; // 1 hour
constructor(redisUrl: string) {
this.redis = new Redis(redisUrl, {
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3,
});
this.redis.on("connect", () => {
console.log("Redis connected");
});
this.redis.on("error", (error) => {
console.error("Redis error:", error);
});
}
/**
* Get cached value
*/
async get<T>(key: string): Promise<T | null> {
try {
const value = await this.redis.get(key);
if (!value) return null;
return JSON.parse(value) as T;
} catch (error) {
console.error(`Cache get error for key ${key}:`, error);
return null;
}
}
/**
* Set cached value
*/
async set(
key: string,
value: any,
options: CacheOptions = {},
): Promise<boolean> {
try {
const ttl = options.ttl || this.defaultTTL;
const serialized = JSON.stringify(value);
if (ttl > 0) {
await this.redis.setex(key, ttl, serialized);
} else {
await this.redis.set(key, serialized);
}
return true;
} catch (error) {
console.error(`Cache set error for key ${key}:`, error);
return false;
}
}
/**
* Delete cached value
*/
async delete(key: string): Promise<boolean> {
try {
await this.redis.del(key);
return true;
} catch (error) {
console.error(`Cache delete error for key ${key}:`, error);
return false;
}
}
/**
* Delete multiple keys by pattern
*/
async deletePattern(pattern: string): Promise<number> {
try {
const keys = await this.redis.keys(pattern);
if (keys.length === 0) return 0;
await this.redis.del(...keys);
return keys.length;
} catch (error) {
console.error(`Cache delete pattern error for ${pattern}:`, error);
return 0;
}
}
/**
* Get or set pattern - fetch from cache or compute and cache
*/
async getOrSet<T>(
key: string,
fetchFn: () => Promise<T>,
options: CacheOptions = {},
): Promise<T> {
// Try to get from cache
const cached = await this.get<T>(key);
if (cached !== null) {
return cached;
}
// Fetch and cache
const value = await fetchFn();
await this.set(key, value, options);
return value;
}
/**
* Implement cache-aside pattern with stale-while-revalidate
*/
async getStaleWhileRevalidate<T>(
key: string,
fetchFn: () => Promise<T>,
options: {
ttl: number;
staleTime: number;
},
): Promise<T> {
const cacheKey = `cache:${key}`;
const timestampKey = `cache:${key}:timestamp`;
const [cached, timestamp] = await Promise.all([
this.get<T>(cacheKey),
this.redis.get(timestampKey),
]);
const now = Date.now();
const age = timestamp ? now - parseInt(timestamp) : Infinity;
// Return cached if fresh
if (cached !== null && age < options.ttl * 1000) {
return cached;
}
// Return stale while revalidating in background
if (cached !== null && age < options.staleTime * 1000) {
// Background revalidation
fetchFn()
.then(async (fresh) => {
await this.set(cacheKey, fresh, { ttl: options.ttl });
await this.redis.set(timestampKey, now.toString());
})
.catch(console.error);
return cached;
}
// Fetch fresh data
const fresh = await fetchFn();
await Promise.all([
this.set(cacheKey, fresh, { ttl: options.ttl }),
this.redis.set(timestampKey, now.toString()),
]);
return fresh;
}
/**
* Increment counter with TTL
*/
async increment(key: string, ttl?: number): Promise<number> {
const count = await this.redis.incr(key);
if (count === 1 && ttl) {
await this.redis.expire(key, ttl);
}
return count;
}
/**
* Check if key exists
*/
async exists(key: string): Promise<boolean> {
const result = await this.redis.exists(key);
return result === 1;
}
/**
* Get remaining TTL
*/
async ttl(key: string): Promise<number> {
return await this.redis.ttl(key);
}
/**
* Close connection
*/
async disconnect(): Promise<void> {
await this.redis.quit();
}
}
// Usage
const cache = new CacheService("redis://localhost:6379");
// Simple get/set
await cache.set("user:123", { name: "John", age: 30 }, { ttl: 3600 });
const user = await cache.get("user:123");
// Get or set pattern
const posts = await cache.getOrSet(
"posts:recent",
async () => {
return await database.query(
"SELECT * FROM posts ORDER BY created_at DESC LIMIT 10",
);
},
{ ttl: 300 },
);
// Stale-while-revalidate
const data = await cache.getStaleWhileRevalidate(
"expensive-query",
async () => await runExpensiveQuery(),
{ ttl: 300, staleTime: 600 },
);#!/bin/bash
# validate-api.sh - Validate API specification
# Usage: ./validate-api.sh <openapi_spec>
set -euo pipefail
SPEC_FILE="${{1:?Usage: $0 <openapi_spec>}}"
echo "Validating API spec: $SPEC_FILE"
# TODO: Add API validation
# - Validate OpenAPI/Swagger syntax
# - Check endpoint naming conventions
# - Verify response schemas
# - Check for required headers
# - Validate authentication definitions
echo "API validation complete."
# API Endpoint Scaffold
# TODO: Customize for your API framework
openapi: "3.0.3"
info:
title: "API Service"
version: "1.0.0"
paths:
/api/v1/resource:
get:
summary: "List resources"
# TODO: Define parameters and responses
responses:
"200":
description: "Success"
post:
summary: "Create resource"
# TODO: Define request body and responses
responses:
"201":
description: "Created"
Related skills
How it compares
Use caching-strategy for full-stack cache architecture and invalidation design rather than dropping Redis on a single endpoint without TTL or busting rules.
FAQ
Which cache technologies does caching-strategy cover?
caching-strategy covers Redis, Memcached, CDN edge caching, and invalidation patterns for backend APIs. The skill helps developers cut database load and improve response times with layered cache design and TTL policies.
When should developers apply caching-strategy?
caching-strategy fits high-read APIs with rising p95 latency or database hot-query saturation. Use it before traffic spikes when origin fetches dominate cost and cached reads can safely reduce load with clear invalidation rules.