Caching & Performance - 1/2

The Performance Nightmare That Kills User Engagement

Picture this catastrophe: Your social media platform just got featured on a major tech blog. Traffic spikes from 1,000 to 100,000 concurrent users in 30 minutes. Your database starts choking:

  • Homepage load time jumps from 200ms to 15 seconds
  • User profile pages timeout after 30 seconds
  • Your database CPU hits 100% and stays there
  • Redis memory usage spikes to 95%
  • Users start abandoning your app en masse
  • Your server costs skyrocket as you desperately add more instances

Meanwhile, your competitors are handling similar traffic with sub-second response times and a fraction of your infrastructure costs.

The worst part? Your code isn’t broken. Your architecture isn’t fundamentally flawed. You simply never learned the performance optimization patterns that separate amateur systems from production-grade applications.

The Uncomfortable Truth About Performance

Here’s what every senior developer learns the hard way: Performance isn’t something you optimize later—it’s an architectural decision you make from day one.

Most developers build applications like this:

  1. Write the feature
  2. Make it work
  3. Deploy to production
  4. Wait for performance complaints
  5. Panic and add more servers

But high-performance systems are built differently:

  1. Understand the performance requirements
  2. Design for the expected load
  3. Implement caching from the start
  4. Monitor and optimize continuously
  5. Scale intelligently, not desperately

The difference between these approaches isn’t just performance—it’s the difference between systems that scale gracefully and systems that collapse under their own weight.

Ready to build applications that perform like they were designed by Netflix instead of your mom’s basement server? Let’s dive into the world of intelligent caching and performance optimization.


Caching Strategies: The Art of Being Lazy (Efficiently)

Understanding Cache Hierarchies

Caching isn’t just “put Redis in front of your database.” It’s a multi-layered strategy that mirrors how your CPU handles memory.

// Poor caching strategy - everything hits the database
class BadUserService {
  async getUser(userId: string): Promise<User> {
    // Every request = database query (expensive!)
    return await this.database.query("SELECT * FROM users WHERE id = $1", [
      userId,
    ]);
  }

  async getUserPosts(userId: string): Promise<Post[]> {
    // Another database hit for every request
    return await this.database.query(
      "SELECT * FROM posts WHERE user_id = $1 ORDER BY created_at DESC",
      [userId]
    );
  }

  async getUserProfile(userId: string): Promise<UserProfile> {
    // Multiple queries for a single profile view
    const user = await this.getUser(userId);
    const posts = await this.getUserPosts(userId);
    const followers = await this.getUserFollowers(userId);
    const following = await this.getUserFollowing(userId);

    return { user, posts, followers, following };
  }
}
// Smart multi-layer caching strategy
class OptimizedUserService {
  private memoryCache: Map<string, any> = new Map();
  private redisCache: Redis;
  private database: Database;

  // Cache hierarchy: Memory → Redis → Database
  async getUser(userId: string): Promise<User> {
    const cacheKey = `user:${userId}`;

    // Layer 1: In-memory cache (fastest, ~1ns access time)
    if (this.memoryCache.has(cacheKey)) {
      this.metrics.increment("cache.memory.hit");
      return this.memoryCache.get(cacheKey);
    }

    // Layer 2: Redis cache (fast, ~1ms access time)
    const cachedUser = await this.redisCache.get(cacheKey);
    if (cachedUser) {
      const user = JSON.parse(cachedUser);

      // Populate memory cache for next request
      this.memoryCache.set(cacheKey, user);
      this.scheduleMemoryCacheEviction(cacheKey, 300000); // 5 minutes

      this.metrics.increment("cache.redis.hit");
      return user;
    }

    // Layer 3: Database (slow, ~50-200ms access time)
    const user = await this.database.query(
      "SELECT * FROM users WHERE id = $1",
      [userId]
    );

    if (user) {
      // Populate all cache layers
      await this.redisCache.setex(cacheKey, 3600, JSON.stringify(user)); // 1 hour
      this.memoryCache.set(cacheKey, user);
      this.scheduleMemoryCacheEviction(cacheKey, 300000);
    }

    this.metrics.increment("cache.database.hit");
    return user;
  }

  async getUserProfile(userId: string): Promise<UserProfile> {
    const profileKey = `profile:${userId}`;

    // Try to get the complete profile from cache first
    const cachedProfile = await this.redisCache.get(profileKey);
    if (cachedProfile) {
      return JSON.parse(cachedProfile);
    }

    // If not cached, build profile and cache the result
    const [user, posts, followers, following] = await Promise.all([
      this.getUser(userId),
      this.getUserPosts(userId),
      this.getUserFollowers(userId),
      this.getUserFollowing(userId),
    ]);

    const profile = { user, posts, followers, following };

    // Cache the complete profile for faster subsequent requests
    await this.redisCache.setex(
      profileKey,
      1800, // 30 minutes (shorter TTL for composite data)
      JSON.stringify(profile)
    );

    return profile;
  }
}

The Cache Pattern Playbook

Different scenarios require different caching strategies. Here’s your decision matrix:

// Cache-Aside Pattern (Lazy Loading)
class CacheAsideService {
  async getData(key: string): Promise<any> {
    // Check cache first
    let data = await this.cache.get(key);

    if (!data) {
      // Cache miss - fetch from source
      data = await this.database.get(key);

      if (data) {
        // Populate cache for next time
        await this.cache.set(key, data, TTL);
      }
    }

    return data;
  }

  async updateData(key: string, newData: any): Promise<void> {
    // Update database first
    await this.database.update(key, newData);

    // Invalidate cache (let next read repopulate)
    await this.cache.delete(key);
  }
}

// Write-Through Pattern (Immediate Consistency)
class WriteThroughService {
  async updateData(key: string, newData: any): Promise<void> {
    // Write to both database and cache simultaneously
    await Promise.all([
      this.database.update(key, newData),
      this.cache.set(key, newData, TTL),
    ]);
  }

  async getData(key: string): Promise<any> {
    // Cache should always have the data
    let data = await this.cache.get(key);

    if (!data) {
      // Cache miss shouldn't happen, but handle it
      data = await this.database.get(key);
      await this.cache.set(key, data, TTL);
    }

    return data;
  }
}

// Write-Behind Pattern (Performance Over Consistency)
class WriteBehindService {
  private writeQueue: Map<string, any> = new Map();

  async updateData(key: string, newData: any): Promise<void> {
    // Update cache immediately
    await this.cache.set(key, newData, TTL);

    // Queue database write for later
    this.writeQueue.set(key, newData);

    // Return immediately (async database update)
  }

  private async processPendingWrites(): Promise<void> {
    const writes = Array.from(this.writeQueue.entries());
    this.writeQueue.clear();

    // Batch write to database
    await this.database.batchUpdate(writes);
  }

  constructor() {
    // Process queued writes every 5 seconds
    setInterval(() => this.processPendingWrites(), 5000);
  }
}

// Read-Through Pattern (Cache as Primary Interface)
class ReadThroughService {
  async getData(key: string): Promise<any> {
    return await this.cache.getWithFallback(key, async () => {
      // This function only called on cache miss
      return await this.database.get(key);
    });
  }
}

When to use each pattern:

  • Cache-Aside: Default choice for most applications
  • Write-Through: When you need immediate consistency
  • Write-Behind: When you need maximum write performance
  • Read-Through: When cache is your primary data interface

In-Memory Caching: Redis vs. Memcached vs. Application Cache

Redis: The Swiss Army Knife of Caching

Redis isn’t just a cache—it’s a data structure server that can replace multiple tools.

// Advanced Redis caching patterns
class RedisOptimizedService {
  private redis: Redis;

  // String caching with compression for large objects
  async cacheUserData(userId: string, userData: any): Promise<void> {
    const key = `user:${userId}`;
    const serialized = JSON.stringify(userData);

    // Compress large objects to save memory
    const compressed =
      serialized.length > 1000 ? await this.compress(serialized) : serialized;

    await this.redis.setex(key, 3600, compressed);
  }

  // Hash operations for partial updates
  async cacheUserProfile(userId: string, profile: UserProfile): Promise<void> {
    const key = `profile:${userId}`;

    // Store as hash for efficient partial updates
    await this.redis.hset(key, {
      name: profile.name,
      email: profile.email,
      lastLogin: profile.lastLogin.toISOString(),
      settings: JSON.stringify(profile.settings),
    });

    await this.redis.expire(key, 7200); // 2 hours
  }

  async updateUserLastLogin(userId: string, loginTime: Date): Promise<void> {
    const key = `profile:${userId}`;

    // Update only one field without fetching entire object
    await this.redis.hset(key, "lastLogin", loginTime.toISOString());
  }

  // List operations for feeds and timelines
  async cacheUserFeed(userId: string, posts: Post[]): Promise<void> {
    const key = `feed:${userId}`;

    // Clear existing feed
    await this.redis.del(key);

    // Add posts as list (ordered by time)
    const postIds = posts.map((post) => post.id);
    if (postIds.length > 0) {
      await this.redis.lpush(key, ...postIds);

      // Keep only last 100 posts in cache
      await this.redis.ltrim(key, 0, 99);

      // Expire feed cache after 1 hour
      await this.redis.expire(key, 3600);
    }
  }

  async addPostToUserFeed(userId: string, postId: string): Promise<void> {
    const key = `feed:${userId}`;

    // Add new post to beginning of feed
    await this.redis.lpush(key, postId);

    // Maintain feed size limit
    await this.redis.ltrim(key, 0, 99);
  }

  // Set operations for relationships and permissions
  async cacheUserPermissions(
    userId: string,
    permissions: string[]
  ): Promise<void> {
    const key = `permissions:${userId}`;

    // Store as set for O(1) membership testing
    if (permissions.length > 0) {
      await this.redis.sadd(key, ...permissions);
      await this.redis.expire(key, 1800); // 30 minutes
    }
  }

  async hasPermission(userId: string, permission: string): Promise<boolean> {
    const key = `permissions:${userId}`;
    return await this.redis.sismember(key, permission);
  }

  // Sorted sets for leaderboards and rankings
  async updateUserScore(userId: string, score: number): Promise<void> {
    await this.redis.zadd("leaderboard", score, userId);
  }

  async getTopUsers(
    limit: number = 10
  ): Promise<Array<{ userId: string; score: number }>> {
    const results = await this.redis.zrevrangebyscore(
      "leaderboard",
      "+inf",
      "-inf",
      "WITHSCORES",
      "LIMIT",
      0,
      limit
    );

    const users = [];
    for (let i = 0; i < results.length; i += 2) {
      users.push({
        userId: results[i],
        score: parseFloat(results[i + 1]),
      });
    }

    return users;
  }

  // Pub/Sub for real-time cache invalidation
  async setupCacheInvalidation(): Promise<void> {
    const subscriber = this.redis.duplicate();

    await subscriber.subscribe("cache:invalidate");

    subscriber.on("message", async (channel, message) => {
      const { pattern, keys } = JSON.parse(message);

      if (pattern === "user:*") {
        // Invalidate all user-related caches
        const userKeys = await this.redis.keys("user:*");
        if (userKeys.length > 0) {
          await this.redis.del(...userKeys);
        }
      } else if (keys) {
        // Invalidate specific keys
        await this.redis.del(...keys);
      }
    });
  }

  async invalidateUserCaches(userId: string): Promise<void> {
    await this.redis.publish(
      "cache:invalidate",
      JSON.stringify({
        keys: [
          `user:${userId}`,
          `profile:${userId}`,
          `feed:${userId}`,
          `permissions:${userId}`,
        ],
      })
    );
  }
}

Memcached: Simple, Fast, Reliable

// Memcached for simple string-based caching
class MemcachedService {
  private memcached: Memcached;

  constructor() {
    this.memcached = new Memcached(
      ["127.0.0.1:11211", "127.0.0.1:11212", "127.0.0.1:11213"],
      {
        // Connection pooling
        maxConnections: 10,

        // Failover configuration
        failures: 5,
        retry: 5000,
        failuresTimeout: 30000,

        // Performance tuning
        reconnect: 18000000,
        timeout: 5000,
        idle: 5000,
      }
    );
  }

  // Consistent hashing for distributed caching
  private getKey(baseKey: string): string {
    // Add prefix for namespacing
    return `myapp:v1:${baseKey}`;
  }

  async get(key: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.memcached.get(this.getKey(key), (err, data) => {
        if (err) {
          reject(err);
        } else {
          resolve(data ? JSON.parse(data) : null);
        }
      });
    });
  }

  async set(key: string, value: any, ttl: number = 3600): Promise<void> {
    return new Promise((resolve, reject) => {
      this.memcached.set(
        this.getKey(key),
        JSON.stringify(value),
        ttl,
        (err) => {
          if (err) reject(err);
          else resolve();
        }
      );
    });
  }

  // Multi-get for batch operations
  async getMultiple(keys: string[]): Promise<Record<string, any>> {
    const prefixedKeys = keys.map((key) => this.getKey(key));

    return new Promise((resolve, reject) => {
      this.memcached.getMulti(prefixedKeys, (err, data) => {
        if (err) {
          reject(err);
        } else {
          const result: Record<string, any> = {};
          for (const [prefixedKey, value] of Object.entries(data)) {
            const originalKey = prefixedKey.replace("myapp:v1:", "");
            result[originalKey] = JSON.parse(value as string);
          }
          resolve(result);
        }
      });
    });
  }
}

Application-Level Caching

// In-memory caching with intelligent eviction
class InMemoryCache {
  private cache: Map<string, CacheEntry> = new Map();
  private maxSize: number;
  private ttlMs: number;

  constructor(maxSize: number = 1000, ttlMs: number = 300000) {
    this.maxSize = maxSize;
    this.ttlMs = ttlMs;

    // Cleanup expired entries every minute
    setInterval(() => this.cleanup(), 60000);
  }

  set(key: string, value: any, customTTL?: number): void {
    // Evict if cache is full
    if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
      this.evictLRU();
    }

    const entry: CacheEntry = {
      value,
      timestamp: Date.now(),
      accessCount: 0,
      lastAccessed: Date.now(),
      ttl: customTTL || this.ttlMs,
    };

    this.cache.set(key, entry);
  }

  get(key: string): any {
    const entry = this.cache.get(key);

    if (!entry) {
      return null;
    }

    // Check if expired
    if (Date.now() - entry.timestamp > entry.ttl) {
      this.cache.delete(key);
      return null;
    }

    // Update access statistics
    entry.accessCount++;
    entry.lastAccessed = Date.now();

    return entry.value;
  }

  // LRU (Least Recently Used) eviction
  private evictLRU(): void {
    let oldestKey: string | null = null;
    let oldestTime = Date.now();

    for (const [key, entry] of this.cache.entries()) {
      if (entry.lastAccessed < oldestTime) {
        oldestTime = entry.lastAccessed;
        oldestKey = key;
      }
    }

    if (oldestKey) {
      this.cache.delete(oldestKey);
    }
  }

  // Cleanup expired entries
  private cleanup(): void {
    const now = Date.now();
    const expired: string[] = [];

    for (const [key, entry] of this.cache.entries()) {
      if (now - entry.timestamp > entry.ttl) {
        expired.push(key);
      }
    }

    expired.forEach((key) => this.cache.delete(key));
  }

  getStats(): CacheStats {
    let totalHits = 0;
    let totalSize = 0;

    for (const entry of this.cache.values()) {
      totalHits += entry.accessCount;
      totalSize += JSON.stringify(entry.value).length;
    }

    return {
      entries: this.cache.size,
      maxSize: this.maxSize,
      totalHits,
      averageHits: totalHits / Math.max(this.cache.size, 1),
      memoryUsage: totalSize,
    };
  }
}

interface CacheEntry {
  value: any;
  timestamp: number;
  accessCount: number;
  lastAccessed: number;
  ttl: number;
}

interface CacheStats {
  entries: number;
  maxSize: number;
  totalHits: number;
  averageHits: number;
  memoryUsage: number;
}

Distributed Caching: Scaling Beyond a Single Machine

Building a Distributed Cache Layer

// Distributed caching with consistent hashing
class DistributedCache {
  private nodes: CacheNode[];
  private hashRing: ConsistentHash;
  private replicationFactor: number;

  constructor(nodes: string[], replicationFactor: number = 2) {
    this.replicationFactor = replicationFactor;
    this.nodes = nodes.map((address) => new CacheNode(address));
    this.hashRing = new ConsistentHash(this.nodes.map((node) => node.address));
  }

  async set(key: string, value: any, ttl: number = 3600): Promise<void> {
    const nodes = this.getNodes(key);

    // Write to multiple nodes for redundancy
    const writePromises = nodes
      .slice(0, this.replicationFactor)
      .map((node) => this.writeToNode(node, key, value, ttl));

    // Wait for majority write (for consistency)
    const majorityCount = Math.floor(this.replicationFactor / 2) + 1;
    const results = await Promise.allSettled(writePromises);

    const successCount = results.filter((r) => r.status === "fulfilled").length;

    if (successCount < majorityCount) {
      throw new Error("Failed to achieve majority write for cache entry");
    }
  }

  async get(key: string): Promise<any> {
    const nodes = this.getNodes(key);

    // Try to read from nodes until we get a value
    for (const node of nodes.slice(0, this.replicationFactor)) {
      try {
        const value = await this.readFromNode(node, key);
        if (value !== null) {
          return value;
        }
      } catch (error) {
        console.warn(`Failed to read from node ${node.address}:`, error);
      }
    }

    return null;
  }

  async delete(key: string): Promise<void> {
    const nodes = this.getNodes(key);

    // Delete from all replica nodes
    const deletePromises = nodes
      .slice(0, this.replicationFactor)
      .map((node) => this.deleteFromNode(node, key));

    await Promise.allSettled(deletePromises);
  }

  private getNodes(key: string): CacheNode[] {
    const nodeAddresses = this.hashRing.getNodes(key, this.replicationFactor);
    return nodeAddresses.map(
      (address) => this.nodes.find((node) => node.address === address)!
    );
  }

  private async writeToNode(
    node: CacheNode,
    key: string,
    value: any,
    ttl: number
  ): Promise<void> {
    if (!node.isHealthy()) {
      throw new Error(`Node ${node.address} is unhealthy`);
    }

    const serialized = JSON.stringify({
      value,
      timestamp: Date.now(),
      ttl: ttl * 1000, // Convert to milliseconds
    });

    await node.client.set(key, serialized, ttl);
  }

  private async readFromNode(node: CacheNode, key: string): Promise<any> {
    if (!node.isHealthy()) {
      return null;
    }

    const data = await node.client.get(key);
    if (!data) return null;

    try {
      const parsed = JSON.parse(data);

      // Check if expired
      if (Date.now() - parsed.timestamp > parsed.ttl) {
        await this.deleteFromNode(node, key);
        return null;
      }

      return parsed.value;
    } catch (error) {
      console.error(`Failed to parse cached data from ${node.address}:`, error);
      return null;
    }
  }

  // Health monitoring and failover
  async startHealthMonitoring(): Promise<void> {
    setInterval(async () => {
      for (const node of this.nodes) {
        try {
          await node.client.ping();
          node.markHealthy();
        } catch (error) {
          console.error(`Node ${node.address} failed health check:`, error);
          node.markUnhealthy();
        }
      }
    }, 10000); // Check every 10 seconds
  }
}

class CacheNode {
  public client: Redis;
  private healthy: boolean = true;
  private lastHealthCheck: Date = new Date();

  constructor(public address: string) {
    this.client = new Redis(address);
  }

  isHealthy(): boolean {
    return this.healthy;
  }

  markHealthy(): void {
    this.healthy = true;
    this.lastHealthCheck = new Date();
  }

  markUnhealthy(): void {
    this.healthy = false;
    this.lastHealthCheck = new Date();
  }
}

Cache Partitioning and Sharding

// Intelligent cache partitioning based on data patterns
class PartitionedCache {
  private hotDataCache: Redis; // Frequently accessed data
  private warmDataCache: Redis; // Occasionally accessed data
  private coldDataCache: Redis; // Rarely accessed data
  private accessTracker: Map<string, AccessMetrics> = new Map();

  constructor() {
    // Different Redis instances optimized for different access patterns
    this.hotDataCache = new Redis({
      host: "hot-cache.redis.cluster",
      maxRetriesPerRequest: 3,
      lazyConnect: true,
    });

    this.warmDataCache = new Redis({
      host: "warm-cache.redis.cluster",
      maxRetriesPerRequest: 2,
      lazyConnect: true,
    });

    this.coldDataCache = new Redis({
      host: "cold-cache.redis.cluster",
      maxRetriesPerRequest: 1,
      lazyConnect: true,
    });

    // Periodically rebalance cache tiers
    setInterval(() => this.rebalanceTiers(), 300000); // Every 5 minutes
  }

  async get(key: string): Promise<any> {
    this.trackAccess(key);

    const tier = this.determineDataTier(key);

    try {
      switch (tier) {
        case "hot":
          return await this.getFromHotCache(key);
        case "warm":
          return await this.getFromWarmCache(key);
        case "cold":
          return await this.getFromColdCache(key);
        default:
          // Try all tiers if tier is unknown
          return await this.getFromAnyTier(key);
      }
    } catch (error) {
      console.error(`Cache get failed for key ${key}:`, error);
      return null;
    }
  }

  async set(key: string, value: any, ttl: number = 3600): Promise<void> {
    const tier = this.determineDataTier(key);
    const cache = this.getCacheForTier(tier);

    await cache.setex(
      key,
      ttl,
      JSON.stringify({
        value,
        tier,
        timestamp: Date.now(),
      })
    );

    this.trackAccess(key);
  }

  private determineDataTier(key: string): "hot" | "warm" | "cold" {
    const metrics = this.accessTracker.get(key);

    if (!metrics) {
      return "cold"; // New keys start cold
    }

    const accessRate =
      metrics.totalAccesses /
      ((Date.now() - metrics.firstAccess) / (1000 * 60 * 60)); // per hour

    if (accessRate > 10) {
      return "hot";
    } else if (accessRate > 1) {
      return "warm";
    } else {
      return "cold";
    }
  }

  private async rebalanceTiers(): Promise<void> {
    const keysToRebalance: Array<{
      key: string;
      currentTier: string;
      targetTier: string;
    }> = [];

    for (const [key, metrics] of this.accessTracker.entries()) {
      const currentTier = metrics.currentTier;
      const targetTier = this.determineDataTier(key);

      if (currentTier !== targetTier) {
        keysToRebalance.push({ key, currentTier, targetTier });
      }
    }

    // Move keys between tiers
    for (const { key, currentTier, targetTier } of keysToRebalance) {
      await this.moveKeyBetweenTiers(key, currentTier, targetTier);
    }

    console.log(`Rebalanced ${keysToRebalance.length} cache keys`);
  }

  private async moveKeyBetweenTiers(
    key: string,
    from: string,
    to: string
  ): Promise<void> {
    const sourceCache = this.getCacheForTier(from);
    const targetCache = this.getCacheForTier(to);

    const data = await sourceCache.get(key);
    if (data) {
      const parsed = JSON.parse(data);
      parsed.tier = to;

      await targetCache.setex(
        key,
        await sourceCache.ttl(key),
        JSON.stringify(parsed)
      );

      await sourceCache.del(key);

      // Update metrics
      const metrics = this.accessTracker.get(key);
      if (metrics) {
        metrics.currentTier = to;
      }
    }
  }

  private trackAccess(key: string): void {
    const now = Date.now();
    let metrics = this.accessTracker.get(key);

    if (!metrics) {
      metrics = {
        totalAccesses: 0,
        firstAccess: now,
        lastAccess: now,
        currentTier: "cold",
      };
      this.accessTracker.set(key, metrics);
    }

    metrics.totalAccesses++;
    metrics.lastAccess = now;
  }
}

interface AccessMetrics {
  totalAccesses: number;
  firstAccess: number;
  lastAccess: number;
  currentTier: string;
}

Cache Invalidation: The Two Hard Things in Computer Science

Smart Cache Invalidation Strategies

// Event-driven cache invalidation
class SmartCacheInvalidator {
  private redis: Redis;
  private dependencies: Map<string, Set<string>> = new Map();
  private invalidationQueue: Queue;

  constructor() {
    this.redis = new Redis();
    this.invalidationQueue = new Queue("cache-invalidation");
    this.setupInvalidationProcessor();
  }

  // Register cache dependencies
  registerDependency(cacheKey: string, dependsOn: string[]): void {
    for (const dependency of dependsOn) {
      if (!this.dependencies.has(dependency)) {
        this.dependencies.set(dependency, new Set());
      }
      this.dependencies.get(dependency)!.add(cacheKey);
    }
  }

  // Invalidate cache with dependency cascade
  async invalidate(
    key: string,
    options: InvalidationOptions = {}
  ): Promise<void> {
    const invalidationId = uuidv4();
    const toInvalidate: Set<string> = new Set([key]);

    // Find all dependent cache keys
    if (options.cascade !== false) {
      this.collectDependencies(key, toInvalidate);
    }

    // Queue invalidation job
    await this.invalidationQueue.add("invalidate-cache", {
      invalidationId,
      keys: Array.from(toInvalidate),
      reason: options.reason || "manual",
      timestamp: Date.now(),
    });

    // If immediate invalidation requested
    if (options.immediate) {
      await this.performInvalidation(Array.from(toInvalidate));
    }
  }

  private collectDependencies(key: string, toInvalidate: Set<string>): void {
    const dependents = this.dependencies.get(key);
    if (dependents) {
      for (const dependent of dependents) {
        if (!toInvalidate.has(dependent)) {
          toInvalidate.add(dependent);
          // Recursively collect dependencies
          this.collectDependencies(dependent, toInvalidate);
        }
      }
    }
  }

  private async performInvalidation(keys: string[]): Promise<void> {
    const pipeline = this.redis.pipeline();

    keys.forEach((key) => {
      pipeline.del(key);
    });

    await pipeline.exec();

    // Publish invalidation event for other cache layers
    await this.redis.publish(
      "cache:invalidated",
      JSON.stringify({
        keys,
        timestamp: Date.now(),
      })
    );
  }

  // Time-based invalidation with smart prefetch
  async scheduleInvalidation(
    key: string,
    invalidateAt: Date,
    prefetchCallback?: () => Promise<any>
  ): Promise<void> {
    const delay = invalidateAt.getTime() - Date.now();

    if (delay > 0) {
      await this.invalidationQueue.add(
        "scheduled-invalidation",
        {
          key,
          prefetchCallback: prefetchCallback
            ? prefetchCallback.toString()
            : null,
        },
        { delay }
      );
    }
  }

  // Probabilistic early expiration to prevent cache stampedes
  async getWithProbabilisticExpiry(
    key: string,
    ttl: number,
    refreshCallback: () => Promise<any>
  ): Promise<any> {
    const data = await this.redis.get(key);

    if (data) {
      const parsed = JSON.parse(data);
      const age = Date.now() - parsed.timestamp;
      const timeLeft = ttl * 1000 - age;

      // Probabilistically refresh before expiration
      // Higher probability as we get closer to expiration
      const refreshProbability = 1 - timeLeft / (ttl * 1000);

      if (Math.random() < refreshProbability) {
        // Refresh in background
        this.refreshInBackground(key, ttl, refreshCallback);
      }

      return parsed.value;
    }

    // Cache miss - fetch and cache
    return await this.fetchAndCache(key, ttl, refreshCallback);
  }

  private async refreshInBackground(
    key: string,
    ttl: number,
    refreshCallback: () => Promise<any>
  ): Promise<void> {
    try {
      const freshValue = await refreshCallback();
      await this.redis.setex(
        key,
        ttl,
        JSON.stringify({
          value: freshValue,
          timestamp: Date.now(),
        })
      );
    } catch (error) {
      console.error(`Background refresh failed for key ${key}:`, error);
    }
  }

  // Cache warming strategies
  async warmCache(patterns: WarmingPattern[]): Promise<void> {
    for (const pattern of patterns) {
      await this.warmCachePattern(pattern);
    }
  }

  private async warmCachePattern(pattern: WarmingPattern): Promise<void> {
    const {
      keyPrefix,
      dataLoader,
      batchSize = 100,
      concurrency = 10,
    } = pattern;

    const keys = await this.getKeysToWarm(keyPrefix);
    const batches = this.chunkArray(keys, batchSize);

    // Process batches with controlled concurrency
    await this.processWithConcurrency(
      batches,
      async (batch) => {
        const data = await dataLoader(batch);
        const pipeline = this.redis.pipeline();

        for (const [key, value] of Object.entries(data)) {
          pipeline.setex(
            key,
            pattern.ttl,
            JSON.stringify({
              value,
              timestamp: Date.now(),
            })
          );
        }

        await pipeline.exec();
      },
      concurrency
    );
  }
}

interface InvalidationOptions {
  cascade?: boolean;
  immediate?: boolean;
  reason?: string;
}

interface WarmingPattern {
  keyPrefix: string;
  dataLoader: (keys: string[]) => Promise<Record<string, any>>;
  ttl: number;
  batchSize?: number;
  concurrency?: number;
}

Tag-Based Cache Invalidation

// Advanced tag-based invalidation system
class TaggedCacheService {
  private redis: Redis;
  private tagPrefix = "tag:";

  async setWithTags(
    key: string,
    value: any,
    ttl: number,
    tags: string[]
  ): Promise<void> {
    const pipeline = this.redis.pipeline();

    // Store the actual cache entry
    pipeline.setex(
      key,
      ttl,
      JSON.stringify({
        value,
        tags,
        timestamp: Date.now(),
      })
    );

    // Associate cache key with each tag
    tags.forEach((tag) => {
      const tagKey = `${this.tagPrefix}${tag}`;
      pipeline.sadd(tagKey, key);
      pipeline.expire(tagKey, ttl + 300); // Keep tags slightly longer
    });

    await pipeline.exec();
  }

  async invalidateByTag(tag: string): Promise<void> {
    const tagKey = `${this.tagPrefix}${tag}`;

    // Get all cache keys associated with this tag
    const cacheKeys = await this.redis.smembers(tagKey);

    if (cacheKeys.length > 0) {
      const pipeline = this.redis.pipeline();

      // Delete all cache entries
      cacheKeys.forEach((cacheKey) => {
        pipeline.del(cacheKey);
      });

      // Remove the tag itself
      pipeline.del(tagKey);

      await pipeline.exec();

      console.log(
        `Invalidated ${cacheKeys.length} cache entries for tag: ${tag}`
      );
    }
  }

  async invalidateByMultipleTags(
    tags: string[],
    operator: "AND" | "OR" = "OR"
  ): Promise<void> {
    const tagKeys = tags.map((tag) => `${this.tagPrefix}${tag}`);

    let keysToInvalidate: string[];

    if (operator === "OR") {
      // Union: invalidate if associated with ANY of the tags
      keysToInvalidate = await this.redis.sunion(...tagKeys);
    } else {
      // Intersection: invalidate only if associated with ALL tags
      keysToInvalidate = await this.redis.sinter(...tagKeys);
    }

    if (keysToInvalidate.length > 0) {
      await this.redis.del(...keysToInvalidate);
      console.log(
        `Invalidated ${
          keysToInvalidate.length
        } cache entries for tags: ${tags.join(", ")}`
      );
    }
  }

  // Example usage patterns
  async cacheUserProfile(userId: string, profile: UserProfile): Promise<void> {
    await this.setWithTags(
      `profile:${userId}`,
      profile,
      3600, // 1 hour
      [
        `user:${userId}`,
        `department:${profile.departmentId}`,
        "user-profiles",
        `role:${profile.role}`,
      ]
    );
  }

  // When user is updated, invalidate all related caches
  async onUserUpdated(userId: string): Promise<void> {
    await this.invalidateByTag(`user:${userId}`);
  }

  // When department changes, invalidate all users in that department
  async onDepartmentChanged(departmentId: string): Promise<void> {
    await this.invalidateByTag(`department:${departmentId}`);
  }

  // When role permissions change, invalidate all users with that role
  async onRolePermissionsChanged(role: string): Promise<void> {
    await this.invalidateByTag(`role:${role}`);
  }
}

Performance Profiling and Optimization

Application Performance Monitoring

// Comprehensive performance monitoring
class PerformanceProfiler {
  private metrics: MetricsCollector;
  private activeProfiles: Map<string, ProfileSession> = new Map();

  // Method-level performance profiling
  profile<T>(name: string, fn: () => Promise<T>): Promise<T> {
    return this.profileAsync(name, fn);
  }

  private async profileAsync<T>(
    name: string,
    fn: () => Promise<T>
  ): Promise<T> {
    const sessionId = uuidv4();
    const session = {
      name,
      startTime: performance.now(),
      startMemory: process.memoryUsage(),
      cpuUsageStart: process.cpuUsage(),
    };

    this.activeProfiles.set(sessionId, session);

    try {
      const result = await fn();
      await this.recordSuccess(sessionId);
      return result;
    } catch (error) {
      await this.recordError(sessionId, error);
      throw error;
    }
  }

  private async recordSuccess(sessionId: string): Promise<void> {
    const session = this.activeProfiles.get(sessionId);
    if (!session) return;

    const endTime = performance.now();
    const endMemory = process.memoryUsage();
    const cpuUsageEnd = process.cpuUsage();

    const duration = endTime - session.startTime;
    const memoryDelta = endMemory.heapUsed - session.startMemory.heapUsed;
    const cpuDelta = {
      user: cpuUsageEnd.user - session.cpuUsageStart.user,
      system: cpuUsageEnd.system - session.cpuUsageStart.system,
    };

    // Record metrics
    this.metrics.histogram(`performance.${session.name}.duration`, duration);
    this.metrics.histogram(
      `performance.${session.name}.memory_delta`,
      memoryDelta
    );
    this.metrics.histogram(
      `performance.${session.name}.cpu_user`,
      cpuDelta.user / 1000
    );
    this.metrics.histogram(
      `performance.${session.name}.cpu_system`,
      cpuDelta.system / 1000
    );

    // Detect performance anomalies
    if (duration > 1000) {
      // Slow operation (>1s)
      this.metrics.increment(`performance.${session.name}.slow_operations`);
    }

    if (memoryDelta > 10 * 1024 * 1024) {
      // High memory usage (>10MB)
      this.metrics.increment(
        `performance.${session.name}.high_memory_operations`
      );
    }

    this.activeProfiles.delete(sessionId);
  }

  // Database query profiling
  async profileDatabaseQuery<T>(
    query: string,
    params: any[],
    executor: () => Promise<T>
  ): Promise<T> {
    const queryHash = this.hashQuery(query);
    const startTime = performance.now();

    try {
      const result = await executor();
      const duration = performance.now() - startTime;

      this.metrics.histogram(`db.query.${queryHash}.duration`, duration);
      this.metrics.increment(`db.query.${queryHash}.success`);

      // Log slow queries
      if (duration > 500) {
        // Slow query threshold
        console.warn("Slow database query detected:", {
          query,
          params,
          duration: `${duration.toFixed(2)}ms`,
        });

        this.metrics.increment("db.slow_queries.total");
      }

      return result;
    } catch (error) {
      this.metrics.increment(`db.query.${queryHash}.error`);
      throw error;
    }
  }

  // Cache performance profiling
  async profileCacheOperation<T>(
    operation: "get" | "set" | "delete",
    key: string,
    executor: () => Promise<T>
  ): Promise<T> {
    const startTime = performance.now();

    try {
      const result = await executor();
      const duration = performance.now() - startTime;

      this.metrics.histogram(`cache.${operation}.duration`, duration);
      this.metrics.increment(`cache.${operation}.success`);

      if (operation === "get") {
        if (result !== null && result !== undefined) {
          this.metrics.increment("cache.hits");
        } else {
          this.metrics.increment("cache.misses");
        }
      }

      return result;
    } catch (error) {
      this.metrics.increment(`cache.${operation}.error`);
      throw error;
    }
  }

  // Memory leak detection
  startMemoryLeakDetection(): void {
    setInterval(() => {
      const usage = process.memoryUsage();

      this.metrics.gauge("memory.heap_used", usage.heapUsed);
      this.metrics.gauge("memory.heap_total", usage.heapTotal);
      this.metrics.gauge("memory.external", usage.external);
      this.metrics.gauge("memory.rss", usage.rss);

      // Alert on memory growth
      const heapUsedMB = usage.heapUsed / (1024 * 1024);
      if (heapUsedMB > 512) {
        // Alert if heap > 512MB
        console.warn(`High memory usage detected: ${heapUsedMB.toFixed(2)}MB`);
      }
    }, 30000); // Check every 30 seconds
  }

  // Generate performance report
  async generatePerformanceReport(): Promise<PerformanceReport> {
    const memoryUsage = process.memoryUsage();
    const cpuUsage = process.cpuUsage();

    return {
      timestamp: new Date(),
      memory: {
        heapUsed: memoryUsage.heapUsed / (1024 * 1024), // MB
        heapTotal: memoryUsage.heapTotal / (1024 * 1024), // MB
        external: memoryUsage.external / (1024 * 1024), // MB
        rss: memoryUsage.rss / (1024 * 1024), // MB
      },
      cpu: {
        user: cpuUsage.user / 1000000, // seconds
        system: cpuUsage.system / 1000000, // seconds
      },
      cacheStats: await this.getCacheStats(),
      databaseStats: await this.getDatabaseStats(),
      activeOperations: this.activeProfiles.size,
    };
  }

  private hashQuery(query: string): string {
    // Simple hash for query identification (remove dynamic values)
    const normalized = query
      .replace(/\$\d+/g, "?") // Replace $1, $2, etc. with ?
      .replace(/\s+/g, " ") // Normalize whitespace
      .trim()
      .toLowerCase();

    return normalized.substring(0, 50); // Truncate for readability
  }
}

interface ProfileSession {
  name: string;
  startTime: number;
  startMemory: NodeJS.MemoryUsage;
  cpuUsageStart: NodeJS.CpuUsage;
}

interface PerformanceReport {
  timestamp: Date;
  memory: {
    heapUsed: number;
    heapTotal: number;
    external: number;
    rss: number;
  };
  cpu: {
    user: number;
    system: number;
  };
  cacheStats: any;
  databaseStats: any;
  activeOperations: number;
}

Key Takeaways

Caching and performance optimization isn’t about adding Redis and calling it done. It’s about understanding data access patterns, designing for scale, and building intelligence into your cache layers.

Essential caching patterns:

  • Multi-layer caching with memory → Redis → database hierarchy
  • Smart invalidation with dependency tracking and tag-based clearing
  • Distributed caching with consistent hashing and replication
  • Performance profiling to identify bottlenecks before they become problems

The caching decision framework:

  • Use in-memory caching for frequently accessed, small data
  • Use Redis for shared cache across multiple instances
  • Use Memcached for simple, high-performance string caching
  • Use distributed caching when you need to scale beyond a single cache server

Cache invalidation strategies:

  • Time-based expiration for data with predictable freshness requirements
  • Event-driven invalidation for data that changes based on user actions
  • Tag-based invalidation for complex dependency relationships
  • Probabilistic early expiration to prevent cache stampedes

What’s Next?

In the next blog, we’ll dive into the operational side of performance optimization—database query optimization, connection pooling, memory management, and the scaling strategies that keep your applications running smoothly under any load.

We’ll also cover the monitoring and alerting systems that help you catch performance degradation before your users do, and the capacity planning techniques that prevent those 3 AM “everything is on fire” moments.

Because building fast systems is only half the battle—keeping them fast in production is where the real expertise shows.