+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 198 of 355

๐Ÿ“˜ Redis with TypeScript: Caching Layer

Master redis with typescript: caching layer in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand Redis caching fundamentals ๐ŸŽฏ
  • Apply Redis caching in real projects ๐Ÿ—๏ธ
  • Debug common caching issues ๐Ÿ›
  • Write type-safe Redis code โœจ

๐ŸŽฏ Introduction

Welcome to this exciting tutorial on Redis with TypeScript! ๐ŸŽ‰ In this guide, weโ€™ll explore how to build lightning-fast caching layers thatโ€™ll make your applications zoom like rockets! ๐Ÿš€

Youโ€™ll discover how Redis can transform your TypeScript applications by dramatically improving performance and user experience. Whether youโ€™re building APIs ๐ŸŒ, web applications ๐Ÿ’ป, or real-time systems โšก, understanding Redis caching is essential for creating scalable, high-performance applications.

By the end of this tutorial, youโ€™ll feel confident implementing Redis caching in your own projects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Redis

๐Ÿค” What is Redis?

Redis is like a super-fast memory box ๐Ÿ“ฆ that stores your data in RAM instead of on disk. Think of it as your computerโ€™s short-term memory - itโ€™s incredibly quick to access but needs power to keep working. Itโ€™s perfect for caching frequently accessed data!

In TypeScript terms, Redis acts as an in-memory key-value store that helps you:

  • โœจ Cache expensive database queries
  • ๐Ÿš€ Store session data for lightning-fast access
  • ๐Ÿ›ก๏ธ Implement rate limiting and security features
  • ๐Ÿ“Š Handle real-time data like leaderboards

๐Ÿ’ก Why Use Redis for Caching?

Hereโ€™s why developers love Redis:

  1. Blazing Fast โšก: Sub-millisecond response times
  2. Type-Safe Integration ๐Ÿ”’: Perfect TypeScript support with proper typing
  3. Rich Data Structures ๐Ÿ“ฆ: Strings, hashes, lists, sets, and more
  4. Persistence Options ๐Ÿ’พ: Keep your cache data safe
  5. Scaling Power ๐Ÿš€: Handle millions of operations per second

Real-world example: Imagine an e-commerce store ๐Ÿ›’. Instead of hitting your database every time someone views product details, Redis caches that data in memory for instant access!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Setting Up Redis with TypeScript

Letโ€™s start with a friendly setup:

// ๐Ÿ‘‹ Hello, Redis + TypeScript!
import { createClient, RedisClientType } from 'redis';

// ๐ŸŽจ Creating our Redis client with types
interface CacheClient {
  client: RedisClientType;
  connect: () => Promise<void>;
  disconnect: () => Promise<void>;
}

class RedisCache implements CacheClient {
  public client: RedisClientType;
  
  constructor(url?: string) {
    // ๐Ÿš€ Create Redis client with connection string
    this.client = createClient({
      url: url || 'redis://localhost:6379'
    });
    
    // ๐Ÿ›ก๏ธ Error handling
    this.client.on('error', (err) => {
      console.log('โŒ Redis Client Error:', err);
    });
  }
  
  // ๐Ÿ”Œ Connect to Redis
  async connect(): Promise<void> {
    await this.client.connect();
    console.log('โœ… Connected to Redis! ๐ŸŽ‰');
  }
  
  // ๐Ÿ”Œ Disconnect from Redis
  async disconnect(): Promise<void> {
    await this.client.disconnect();
    console.log('๐Ÿ‘‹ Disconnected from Redis!');
  }
}

๐Ÿ’ก Explanation: Notice how we define proper TypeScript interfaces for our Redis client! This gives us autocomplete and type safety.

๐ŸŽฏ Basic Caching Operations

Here are the essential caching patterns:

// ๐Ÿ—๏ธ Generic cache operations
interface CacheValue {
  data: any;
  timestamp: number;
  ttl?: number; // โฐ Time to live in seconds
}

class TypeSafeCache extends RedisCache {
  
  // ๐Ÿ’พ Set cache with TTL
  async set<T>(key: string, value: T, ttlSeconds: number = 3600): Promise<void> {
    const cacheValue: CacheValue = {
      data: value,
      timestamp: Date.now(),
      ttl: ttlSeconds
    };
    
    await this.client.setEx(key, ttlSeconds, JSON.stringify(cacheValue));
    console.log(`โœ… Cached: ${key} (TTL: ${ttlSeconds}s) ๐Ÿ“ฆ`);
  }
  
  // ๐Ÿ“– Get from cache with type safety
  async get<T>(key: string): Promise<T | null> {
    const cached = await this.client.get(key);
    
    if (!cached) {
      console.log(`โŒ Cache miss: ${key} ๐Ÿ’จ`);
      return null;
    }
    
    const cacheValue: CacheValue = JSON.parse(cached);
    console.log(`โœ… Cache hit: ${key} โšก`);
    return cacheValue.data as T;
  }
  
  // ๐Ÿ—‘๏ธ Delete from cache
  async delete(key: string): Promise<boolean> {
    const result = await this.client.del(key);
    console.log(result ? `โœ… Deleted: ${key} ๐Ÿ—‘๏ธ` : `โŒ Not found: ${key}`);
    return result > 0;
  }
  
  // โฐ Check if key exists
  async exists(key: string): Promise<boolean> {
    const exists = await this.client.exists(key);
    return exists === 1;
  }
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-commerce Product Cache

Letโ€™s build a real caching system for an online store:

// ๐Ÿ›๏ธ Product interface
interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  category: string;
  inStock: boolean;
  emoji: string; // Every product needs personality! 
}

// ๐Ÿช Product service with caching
class ProductService {
  private cache: TypeSafeCache;
  private readonly CACHE_TTL = 1800; // ๐Ÿ• 30 minutes
  
  constructor(cache: TypeSafeCache) {
    this.cache = cache;
  }
  
  // ๐Ÿ“ฆ Get product with caching
  async getProduct(productId: string): Promise<Product | null> {
    const cacheKey = `product:${productId}`;
    
    // ๐Ÿ” Try cache first
    let product = await this.cache.get<Product>(cacheKey);
    
    if (product) {
      console.log(`๐Ÿš€ Served from cache: ${product.emoji} ${product.name}`);
      return product;
    }
    
    // ๐Ÿ’พ Cache miss - fetch from database
    product = await this.fetchProductFromDatabase(productId);
    
    if (product) {
      // ๐Ÿ“ฆ Cache for next time
      await this.cache.set(cacheKey, product, this.CACHE_TTL);
      console.log(`๐Ÿ’พ Cached product: ${product.emoji} ${product.name}`);
    }
    
    return product;
  }
  
  // ๐Ÿท๏ธ Get products by category (with bulk caching)
  async getProductsByCategory(category: string): Promise<Product[]> {
    const cacheKey = `category:${category}`;
    
    // ๐Ÿ” Check cache
    let products = await this.cache.get<Product[]>(cacheKey);
    
    if (products) {
      console.log(`๐Ÿš€ Category cache hit: ${category} (${products.length} products)`);
      return products;
    }
    
    // ๐Ÿ’พ Fetch from database
    products = await this.fetchProductsByCategoryFromDatabase(category);
    
    // ๐Ÿ“ฆ Cache the results
    await this.cache.set(cacheKey, products, this.CACHE_TTL);
    console.log(`๐Ÿ’พ Cached category: ${category} with ${products.length} products`);
    
    return products;
  }
  
  // ๐Ÿ”„ Update product (invalidate cache)
  async updateProduct(productId: string, updates: Partial<Product>): Promise<Product | null> {
    // ๐Ÿ—‘๏ธ Remove from cache first
    await this.cache.delete(`product:${productId}`);
    
    // ๐Ÿ’พ Update in database
    const updatedProduct = await this.updateProductInDatabase(productId, updates);
    
    if (updatedProduct) {
      // ๐Ÿ“ฆ Cache the updated product
      await this.cache.set(`product:${productId}`, updatedProduct, this.CACHE_TTL);
      console.log(`๐Ÿ”„ Updated and cached: ${updatedProduct.emoji} ${updatedProduct.name}`);
    }
    
    return updatedProduct;
  }
  
  // ๐ŸŽญ Mock database methods (replace with real DB calls)
  private async fetchProductFromDatabase(id: string): Promise<Product | null> {
    // ๐ŸŽฎ Simulate database delay
    await new Promise(resolve => setTimeout(resolve, 100));
    
    return {
      id,
      name: "TypeScript Masterclass",
      price: 99.99,
      description: "Learn TypeScript like a pro!",
      category: "education",
      inStock: true,
      emoji: "๐Ÿ“˜"
    };
  }
  
  private async fetchProductsByCategoryFromDatabase(category: string): Promise<Product[]> {
    await new Promise(resolve => setTimeout(resolve, 150));
    
    return [
      {
        id: "1",
        name: "Redis Guide",
        price: 49.99,
        description: "Master Redis caching",
        category,
        inStock: true,
        emoji: "๐Ÿ“ฆ"
      },
      {
        id: "2", 
        name: "TypeScript Handbook",
        price: 79.99,
        description: "Complete TypeScript reference",
        category,
        inStock: true,
        emoji: "๐Ÿ“–"
      }
    ];
  }
  
  private async updateProductInDatabase(id: string, updates: Partial<Product>): Promise<Product | null> {
    await new Promise(resolve => setTimeout(resolve, 80));
    
    return {
      id,
      name: "Updated Product",
      price: 59.99,
      description: "Freshly updated!",
      category: "books",
      inStock: true,
      emoji: "โœจ",
      ...updates
    };
  }
}

๐ŸŽฏ Try it yourself: Add a โ€œhot productsโ€ cache that stores the most viewed items!

๐ŸŽฎ Example 2: User Session Management

Letโ€™s implement a session cache system:

// ๐Ÿ‘ค User session interface
interface UserSession {
  userId: string;
  username: string;
  email: string;
  roles: string[];
  loginTime: Date;
  lastActivity: Date;
  preferences: {
    theme: 'light' | 'dark';
    language: string;
    notifications: boolean;
  };
}

// ๐Ÿ” Session manager with Redis
class SessionManager {
  private cache: TypeSafeCache;
  private readonly SESSION_TTL = 86400; // ๐Ÿ• 24 hours
  
  constructor(cache: TypeSafeCache) {
    this.cache = cache;
  }
  
  // ๐ŸŽซ Create new session
  async createSession(userId: string, userInfo: Omit<UserSession, 'userId' | 'loginTime' | 'lastActivity'>): Promise<string> {
    const sessionId = this.generateSessionId();
    const session: UserSession = {
      userId,
      ...userInfo,
      loginTime: new Date(),
      lastActivity: new Date()
    };
    
    const sessionKey = `session:${sessionId}`;
    const userSessionKey = `user_sessions:${userId}`;
    
    // ๐Ÿ’พ Store session data
    await this.cache.set(sessionKey, session, this.SESSION_TTL);
    
    // ๐Ÿ“ Track user's active sessions
    const activeSessions = await this.cache.get<string[]>(userSessionKey) || [];
    activeSessions.push(sessionId);
    await this.cache.set(userSessionKey, activeSessions, this.SESSION_TTL);
    
    console.log(`โœ… Created session for ${userInfo.username}: ${sessionId} ๐ŸŽซ`);
    return sessionId;
  }
  
  // ๐Ÿ” Get session
  async getSession(sessionId: string): Promise<UserSession | null> {
    const sessionKey = `session:${sessionId}`;
    const session = await this.cache.get<UserSession>(sessionKey);
    
    if (session) {
      // ๐Ÿ”„ Update last activity
      session.lastActivity = new Date();
      await this.cache.set(sessionKey, session, this.SESSION_TTL);
      console.log(`โšก Session accessed: ${session.username}`);
    }
    
    return session;
  }
  
  // ๐Ÿ—‘๏ธ Destroy session
  async destroySession(sessionId: string): Promise<boolean> {
    const session = await this.getSession(sessionId);
    
    if (session) {
      // ๐Ÿ—‘๏ธ Remove session
      await this.cache.delete(`session:${sessionId}`);
      
      // ๐Ÿ“ Update user's session list
      const userSessionKey = `user_sessions:${session.userId}`;
      const activeSessions = await this.cache.get<string[]>(userSessionKey) || [];
      const updatedSessions = activeSessions.filter(id => id !== sessionId);
      
      if (updatedSessions.length > 0) {
        await this.cache.set(userSessionKey, updatedSessions, this.SESSION_TTL);
      } else {
        await this.cache.delete(userSessionKey);
      }
      
      console.log(`๐Ÿ—‘๏ธ Destroyed session: ${session.username}`);
      return true;
    }
    
    return false;
  }
  
  // ๐Ÿ“Š Get user's active sessions
  async getUserSessions(userId: string): Promise<UserSession[]> {
    const userSessionKey = `user_sessions:${userId}`;
    const sessionIds = await this.cache.get<string[]>(userSessionKey) || [];
    
    const sessions: UserSession[] = [];
    
    for (const sessionId of sessionIds) {
      const session = await this.cache.get<UserSession>(`session:${sessionId}`);
      if (session) {
        sessions.push(session);
      }
    }
    
    console.log(`๐Ÿ“Š Found ${sessions.length} active sessions for user ${userId}`);
    return sessions;
  }
  
  // ๐ŸŽฒ Generate unique session ID
  private generateSessionId(): string {
    return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: Cache Patterns & Strategies

When youโ€™re ready to level up, try these advanced caching patterns:

// ๐ŸŽฏ Cache-aside pattern with automatic refresh
class AdvancedCacheManager<T> {
  private cache: TypeSafeCache;
  private refreshCallbacks: Map<string, () => Promise<T>> = new Map();
  
  constructor(cache: TypeSafeCache) {
    this.cache = cache;
  }
  
  // ๐Ÿ”„ Cache-aside with background refresh
  async getWithRefresh<T>(
    key: string, 
    fetchFunction: () => Promise<T>,
    ttl: number = 3600,
    refreshThreshold: number = 0.8 // ๐Ÿ“Š Refresh when 80% of TTL passed
  ): Promise<T> {
    
    // ๐Ÿ” Try cache first
    const cached = await this.cache.get<{data: T, cachedAt: number}>(key);
    
    if (cached) {
      const age = Date.now() - cached.cachedAt;
      const refreshTime = ttl * 1000 * refreshThreshold;
      
      // ๐Ÿ”„ Background refresh if approaching expiry
      if (age > refreshTime && !this.refreshCallbacks.has(key)) {
        this.refreshInBackground(key, fetchFunction, ttl);
      }
      
      return cached.data;
    }
    
    // ๐Ÿ’พ Cache miss - fetch and cache
    const freshData = await fetchFunction();
    await this.cacheWithTimestamp(key, freshData, ttl);
    
    return freshData;
  }
  
  // ๐ŸŒ™ Background refresh (non-blocking)
  private async refreshInBackground<T>(
    key: string, 
    fetchFunction: () => Promise<T>,
    ttl: number
  ): Promise<void> {
    this.refreshCallbacks.set(key, fetchFunction);
    
    try {
      console.log(`๐Ÿ”„ Background refresh started: ${key}`);
      const freshData = await fetchFunction();
      await this.cacheWithTimestamp(key, freshData, ttl);
      console.log(`โœ… Background refresh completed: ${key} ๐ŸŒŸ`);
    } catch (error) {
      console.log(`โŒ Background refresh failed: ${key}`, error);
    } finally {
      this.refreshCallbacks.delete(key);
    }
  }
  
  // ๐Ÿ“ฆ Cache with timestamp
  private async cacheWithTimestamp<T>(key: string, data: T, ttl: number): Promise<void> {
    const cacheData = {
      data,
      cachedAt: Date.now()
    };
    
    await this.cache.set(key, cacheData, ttl);
  }
}

๐Ÿ—๏ธ Advanced Topic 2: Distributed Cache Patterns

For enterprise applications, try distributed caching:

// ๐ŸŒ Distributed cache with Redis Cluster
interface CacheNode {
  host: string;
  port: number;
  role: 'master' | 'replica';
}

class DistributedCache {
  private nodes: Map<string, RedisClientType> = new Map();
  
  constructor(nodes: CacheNode[]) {
    this.initializeNodes(nodes);
  }
  
  // ๐ŸŽฏ Consistent hashing for key distribution
  private getNodeForKey(key: string): RedisClientType {
    const hash = this.hashKey(key);
    const nodeKeys = Array.from(this.nodes.keys());
    const nodeIndex = hash % nodeKeys.length;
    
    const selectedNode = this.nodes.get(nodeKeys[nodeIndex]);
    if (!selectedNode) {
      throw new Error('โŒ No available cache nodes!');
    }
    
    return selectedNode;
  }
  
  // ๐Ÿ”ง Initialize connection to all nodes
  private async initializeNodes(nodes: CacheNode[]): Promise<void> {
    for (const node of nodes) {
      const client = createClient({
        socket: {
          host: node.host,
          port: node.port
        }
      });
      
      await client.connect();
      this.nodes.set(`${node.host}:${node.port}`, client);
      console.log(`โœ… Connected to cache node: ${node.host}:${node.port} ๐ŸŒ`);
    }
  }
  
  // ๐Ÿ”ข Simple hash function
  private hashKey(key: string): number {
    let hash = 0;
    for (let i = 0; i < key.length; i++) {
      const char = key.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // Convert to 32-bit integer
    }
    return Math.abs(hash);
  }
  
  // ๐Ÿ“ฆ Distributed set
  async set(key: string, value: any, ttl: number): Promise<void> {
    const node = this.getNodeForKey(key);
    await node.setEx(key, ttl, JSON.stringify(value));
    console.log(`๐Ÿ“ฆ Distributed cache set: ${key} โšก`);
  }
  
  // ๐Ÿ” Distributed get
  async get<T>(key: string): Promise<T | null> {
    const node = this.getNodeForKey(key);
    const result = await node.get(key);
    
    if (result) {
      console.log(`๐ŸŽฏ Distributed cache hit: ${key} ๐Ÿš€`);
      return JSON.parse(result) as T;
    }
    
    console.log(`๐Ÿ’จ Distributed cache miss: ${key}`);
    return null;
  }
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Cache Stampede

// โŒ Wrong way - multiple requests fetch the same data!
class BadCache {
  async getData(key: string): Promise<any> {
    let data = await this.cache.get(key);
    if (!data) {
      // ๐Ÿ’ฅ All requests will hit the database simultaneously!
      data = await this.expensiveDatabaseQuery();
      await this.cache.set(key, data, 3600);
    }
    return data;
  }
}

// โœ… Correct way - use locks to prevent stampede!
class GoodCache {
  private lockMap: Map<string, Promise<any>> = new Map();
  
  async getData(key: string): Promise<any> {
    let data = await this.cache.get(key);
    if (data) return data;
    
    // ๐Ÿ”’ Check if someone else is already fetching
    if (this.lockMap.has(key)) {
      console.log(`โณ Waiting for lock: ${key}`);
      return await this.lockMap.get(key);
    }
    
    // ๐Ÿ” Acquire lock and fetch data
    const fetchPromise = this.fetchWithLock(key);
    this.lockMap.set(key, fetchPromise);
    
    try {
      data = await fetchPromise;
      return data;
    } finally {
      this.lockMap.delete(key); // ๐Ÿ”“ Release lock
    }
  }
  
  private async fetchWithLock(key: string): Promise<any> {
    console.log(`๐Ÿ” Acquired lock for: ${key}`);
    const data = await this.expensiveDatabaseQuery();
    await this.cache.set(key, data, 3600);
    console.log(`โœ… Cached with lock: ${key} ๐ŸŽ‰`);
    return data;
  }
}

๐Ÿคฏ Pitfall 2: Memory Leaks with Large Objects

// โŒ Dangerous - caching huge objects without limits!
class DangerousCache {
  async cacheUserData(userId: string, userData: any): Promise<void> {
    // ๐Ÿ’ฅ Could cache massive objects eating all memory!
    await this.cache.set(`user:${userId}`, userData, 3600);
  }
}

// โœ… Safe - implement size limits and compression!
class SafeCache {
  private readonly MAX_CACHE_SIZE = 1024 * 1024; // ๐Ÿ“ 1MB limit
  
  async cacheUserData(userId: string, userData: any): Promise<void> {
    const serialized = JSON.stringify(userData);
    
    // ๐Ÿ“ Check size before caching
    if (serialized.length > this.MAX_CACHE_SIZE) {
      console.log(`โš ๏ธ Object too large to cache: ${userId} (${serialized.length} bytes)`);
      return;
    }
    
    // ๐Ÿ—œ๏ธ Optional: compress large objects
    const compressed = serialized.length > 1024 ? 
      await this.compress(serialized) : serialized;
    
    await this.cache.set(`user:${userId}`, compressed, 3600);
    console.log(`โœ… Safely cached: ${userId} (${compressed.length} bytes) ๐Ÿ’พ`);
  }
  
  private async compress(data: string): Promise<string> {
    // ๐Ÿ—œ๏ธ Use zlib or similar compression
    console.log(`๐Ÿ—œ๏ธ Compressing data...`);
    return data; // Simplified - implement real compression
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Use Appropriate TTLs: Short for frequently changing data, longer for stable data
  2. ๐Ÿ“ Implement Cache Keys Carefully: Use consistent, hierarchical naming (user:123:profile)
  3. ๐Ÿ›ก๏ธ Handle Cache Failures Gracefully: Always have fallback to original data source
  4. ๐ŸŽจ Type Everything: Use proper TypeScript interfaces for cached data
  5. โšก Monitor Cache Performance: Track hit rates, memory usage, and response times
  6. ๐Ÿ”„ Implement Cache Invalidation: Update or remove stale data promptly
  7. ๐Ÿ“Š Use Cache Layers: L1 (in-memory), L2 (Redis), L3 (database)

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Blog Post Cache System

Create a comprehensive caching system for a blog platform:

๐Ÿ“‹ Requirements:

  • โœ… Cache blog posts with automatic expiration (30 minutes)
  • ๐Ÿท๏ธ Cache posts by category and tags
  • ๐Ÿ‘ค Cache author information separately (4 hours TTL)
  • ๐Ÿ“Š Implement view count caching with atomic increments
  • ๐Ÿ” Add search result caching (15 minutes)
  • โšก Handle cache invalidation when posts are updated
  • ๐ŸŽจ Include TypeScript interfaces for all data structures

๐Ÿš€ Bonus Points:

  • Add cache warming for popular posts
  • Implement cache size monitoring
  • Create a cache analytics dashboard
  • Add distributed caching support

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Our type-safe blog cache system!
interface BlogPost {
  id: string;
  title: string;
  content: string;
  authorId: string;
  category: string;
  tags: string[];
  publishedAt: Date;
  updatedAt: Date;
  viewCount: number;
  featured: boolean;
  emoji: string;
}

interface Author {
  id: string;
  name: string;
  email: string;
  bio: string;
  avatar: string;
  postsCount: number;
}

interface SearchResult {
  query: string;
  posts: BlogPost[];
  totalCount: number;
  searchedAt: Date;
}

class BlogCacheManager {
  private cache: TypeSafeCache;
  
  // โฐ Cache TTL constants
  private readonly POST_TTL = 1800;      // 30 minutes
  private readonly AUTHOR_TTL = 14400;   // 4 hours
  private readonly SEARCH_TTL = 900;     // 15 minutes
  private readonly VIEWS_TTL = 86400;    // 24 hours
  
  constructor(cache: TypeSafeCache) {
    this.cache = cache;
  }
  
  // ๐Ÿ“ Get blog post with caching
  async getPost(postId: string): Promise<BlogPost | null> {
    const cacheKey = `post:${postId}`;
    
    // ๐Ÿ” Try cache first
    let post = await this.cache.get<BlogPost>(cacheKey);
    
    if (post) {
      console.log(`๐Ÿš€ Post cache hit: ${post.emoji} ${post.title}`);
      // ๐Ÿ“Š Increment view count atomically
      await this.incrementViewCount(postId);
      return post;
    }
    
    // ๐Ÿ’พ Cache miss - fetch from database
    post = await this.fetchPostFromDatabase(postId);
    
    if (post) {
      await this.cache.set(cacheKey, post, this.POST_TTL);
      await this.incrementViewCount(postId);
      console.log(`๐Ÿ’พ Cached post: ${post.emoji} ${post.title}`);
    }
    
    return post;
  }
  
  // ๐Ÿ‘ค Get author with caching
  async getAuthor(authorId: string): Promise<Author | null> {
    const cacheKey = `author:${authorId}`;
    
    let author = await this.cache.get<Author>(cacheKey);
    
    if (author) {
      console.log(`๐Ÿ‘ค Author cache hit: ${author.name}`);
      return author;
    }
    
    author = await this.fetchAuthorFromDatabase(authorId);
    
    if (author) {
      await this.cache.set(cacheKey, author, this.AUTHOR_TTL);
      console.log(`๐Ÿ’พ Cached author: ${author.name}`);
    }
    
    return author;
  }
  
  // ๐Ÿท๏ธ Get posts by category
  async getPostsByCategory(category: string): Promise<BlogPost[]> {
    const cacheKey = `category:${category}`;
    
    let posts = await this.cache.get<BlogPost[]>(cacheKey);
    
    if (posts) {
      console.log(`๐Ÿท๏ธ Category cache hit: ${category} (${posts.length} posts)`);
      return posts;
    }
    
    posts = await this.fetchPostsByCategoryFromDatabase(category);
    await this.cache.set(cacheKey, posts, this.POST_TTL);
    console.log(`๐Ÿ’พ Cached category: ${category} with ${posts.length} posts`);
    
    return posts;
  }
  
  // ๐Ÿ” Search posts with caching
  async searchPosts(query: string): Promise<SearchResult> {
    const cacheKey = `search:${this.normalizeSearchQuery(query)}`;
    
    let result = await this.cache.get<SearchResult>(cacheKey);
    
    if (result) {
      console.log(`๐Ÿ” Search cache hit: "${query}" (${result.posts.length} results)`);
      return result;
    }
    
    const posts = await this.performSearchInDatabase(query);
    result = {
      query,
      posts,
      totalCount: posts.length,
      searchedAt: new Date()
    };
    
    await this.cache.set(cacheKey, result, this.SEARCH_TTL);
    console.log(`๐Ÿ’พ Cached search: "${query}" with ${posts.length} results`);
    
    return result;
  }
  
  // ๐Ÿ“Š Increment view count atomically
  async incrementViewCount(postId: string): Promise<number> {
    const viewsKey = `views:${postId}`;
    
    // ๐Ÿ”ข Use Redis INCR for atomic increment
    const newCount = await this.cache.client.incr(viewsKey);
    await this.cache.client.expire(viewsKey, this.VIEWS_TTL);
    
    return newCount;
  }
  
  // ๐Ÿ—‘๏ธ Invalidate post cache when updated
  async invalidatePost(postId: string): Promise<void> {
    const post = await this.cache.get<BlogPost>(`post:${postId}`);
    
    if (post) {
      // ๐Ÿ—‘๏ธ Remove post cache
      await this.cache.delete(`post:${postId}`);
      
      // ๐Ÿ—‘๏ธ Remove category cache
      await this.cache.delete(`category:${post.category}`);
      
      // ๐Ÿ—‘๏ธ Remove tag caches
      for (const tag of post.tags) {
        await this.cache.delete(`tag:${tag}`);
      }
      
      console.log(`๐Ÿ—‘๏ธ Invalidated caches for post: ${post.title}`);
    }
  }
  
  // ๐Ÿ”ฅ Cache warming for popular posts
  async warmCache(popularPostIds: string[]): Promise<void> {
    console.log(`๐Ÿ”ฅ Warming cache for ${popularPostIds.length} popular posts...`);
    
    const warmingPromises = popularPostIds.map(async (postId) => {
      const post = await this.fetchPostFromDatabase(postId);
      if (post) {
        await this.cache.set(`post:${postId}`, post, this.POST_TTL);
        console.log(`๐Ÿ”ฅ Warmed cache: ${post.emoji} ${post.title}`);
      }
    });
    
    await Promise.all(warmingPromises);
    console.log(`โœ… Cache warming completed! ๐ŸŽ‰`);
  }
  
  // ๐Ÿ“Š Get cache statistics
  async getCacheStats(): Promise<{hits: number, misses: number, size: number}> {
    // ๐Ÿ“Š Mock implementation - in real app, track these metrics
    return {
      hits: 1542,
      misses: 238,
      size: 1024 * 1024 * 15 // 15MB
    };
  }
  
  // ๐Ÿ”ง Helper methods
  private normalizeSearchQuery(query: string): string {
    return query.toLowerCase().trim().replace(/\s+/g, '-');
  }
  
  // ๐ŸŽญ Mock database methods
  private async fetchPostFromDatabase(id: string): Promise<BlogPost | null> {
    await new Promise(resolve => setTimeout(resolve, 100));
    
    return {
      id,
      title: "Redis Caching Mastery",
      content: "Learn how to build lightning-fast applications...",
      authorId: "author-123",
      category: "tutorials",
      tags: ["redis", "typescript", "caching"],
      publishedAt: new Date('2024-01-15'),
      updatedAt: new Date('2024-01-16'),
      viewCount: 0,
      featured: true,
      emoji: "๐Ÿš€"
    };
  }
  
  private async fetchAuthorFromDatabase(id: string): Promise<Author | null> {
    await new Promise(resolve => setTimeout(resolve, 80));
    
    return {
      id,
      name: "Sarah TypeScript",
      email: "[email protected]",
      bio: "Full-stack developer passionate about TypeScript",
      avatar: "https://example.com/sarah.jpg",
      postsCount: 42
    };
  }
  
  private async fetchPostsByCategoryFromDatabase(category: string): Promise<BlogPost[]> {
    await new Promise(resolve => setTimeout(resolve, 120));
    
    return [
      {
        id: "post-1",
        title: "TypeScript Advanced Types",
        content: "Master complex type manipulations...",
        authorId: "author-123",
        category,
        tags: ["typescript", "types"],
        publishedAt: new Date(),
        updatedAt: new Date(),
        viewCount: 156,
        featured: false,
        emoji: "๐Ÿ“˜"
      },
      {
        id: "post-2",
        title: "Redis Performance Tips",
        content: "Optimize your Redis usage...",
        authorId: "author-456",
        category,
        tags: ["redis", "performance"],
        publishedAt: new Date(),
        updatedAt: new Date(),
        viewCount: 89,
        featured: true,
        emoji: "โšก"
      }
    ];
  }
  
  private async performSearchInDatabase(query: string): Promise<BlogPost[]> {
    await new Promise(resolve => setTimeout(resolve, 200));
    
    return [
      {
        id: "search-result-1",
        title: `Search Result for: ${query}`,
        content: "This post matches your search query...",
        authorId: "author-789",
        category: "search-results",
        tags: [query.toLowerCase()],
        publishedAt: new Date(),
        updatedAt: new Date(),
        viewCount: 23,
        featured: false,
        emoji: "๐Ÿ”"
      }
    ];
  }
}

// ๐ŸŽฎ Usage example
async function demonstrateBlogCache() {
  const cache = new TypeSafeCache();
  await cache.connect();
  
  const blogCache = new BlogCacheManager(cache);
  
  // ๐Ÿ”ฅ Warm the cache
  await blogCache.warmCache(['post-1', 'post-2', 'post-3']);
  
  // ๐Ÿ“ Get posts (will be served from cache)
  const post = await blogCache.getPost('post-1');
  const author = await blogCache.getAuthor('author-123');
  const categoryPosts = await blogCache.getPostsByCategory('tutorials');
  const searchResults = await blogCache.searchPosts('TypeScript Redis');
  
  // ๐Ÿ“Š Check stats
  const stats = await blogCache.getCacheStats();
  console.log('๐Ÿ“Š Cache Stats:', stats);
  
  await cache.disconnect();
}

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Implement Redis caching with complete type safety ๐Ÿ’ช
  • โœ… Avoid cache stampedes and other common pitfalls ๐Ÿ›ก๏ธ
  • โœ… Build scalable caching systems for real applications ๐ŸŽฏ
  • โœ… Debug caching issues like a pro ๐Ÿ›
  • โœ… Create lightning-fast applications with Redis! ๐Ÿš€

Remember: Redis caching is like having a superpower for your applications - use it wisely and your users will thank you for the blazing-fast performance! โšก

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered Redis caching with TypeScript!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the blog cache exercise above
  2. ๐Ÿ—๏ธ Build a caching layer for your own project
  3. ๐Ÿ“š Move on to our next tutorial: Database ORMs with TypeScript
  4. ๐ŸŒŸ Share your caching success stories with the community!

Remember: Every high-performance application uses caching effectively. You now have the skills to build systems that can handle millions of users! Keep coding, keep caching, and most importantly, have fun! ๐Ÿš€


Happy caching! ๐ŸŽ‰๐Ÿš€โœจ