Skip to content

Python Caching Strategies Best Practices

Objective: Master senior-level Python caching patterns for production systems. When you need to implement high-performance caching, when you want to build distributed cache systems, when you need enterprise-grade caching strategiesโ€”these best practices become your weapon of choice.

Core Principles

  • Performance: Optimize for speed and throughput
  • Consistency: Balance cache consistency with performance
  • Scalability: Design for horizontal scaling
  • Reliability: Implement fault tolerance and fallback mechanisms
  • Security: Protect cached data and prevent cache poisoning

Multi-Level Caching

Cache Architecture

# python/01-multi-level-caching.py

"""
Multi-level caching architecture and strategies
"""

from typing import List, Dict, Any, Optional, Union, Tuple, Callable
from dataclasses import dataclass, asdict
from enum import Enum
import time
import json
import hashlib
import pickle
import threading
from datetime import datetime, timedelta
import logging
from collections import OrderedDict
import redis
from redis import ConnectionPool as RedisConnectionPool
import memcached
from functools import wraps
import asyncio
import aioredis
from contextlib import asynccontextmanager

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class CacheLevel(Enum):
    """Cache level enumeration"""
    L1 = "l1"  # In-memory cache
    L2 = "l2"  # Redis cache
    L3 = "l3"  # Database cache

class CacheStrategy(Enum):
    """Cache strategy enumeration"""
    LRU = "lru"
    LFU = "lfu"
    TTL = "ttl"
    WRITE_THROUGH = "write_through"
    WRITE_BACK = "write_back"

@dataclass
class CacheEntry:
    """Cache entry definition"""
    key: str
    value: Any
    created_at: datetime
    accessed_at: datetime
    access_count: int = 0
    ttl: Optional[int] = None
    size: int = 0

    def is_expired(self) -> bool:
        """Check if cache entry is expired"""
        if self.ttl is None:
            return False
        return datetime.utcnow() > self.created_at + timedelta(seconds=self.ttl)

    def update_access(self) -> None:
        """Update access information"""
        self.accessed_at = datetime.utcnow()
        self.access_count += 1

class L1Cache:
    """L1 In-memory cache implementation"""

    def __init__(self, max_size: int = 1000, strategy: CacheStrategy = CacheStrategy.LRU):
        self.max_size = max_size
        self.strategy = strategy
        self.cache: OrderedDict[str, CacheEntry] = OrderedDict()
        self.lock = threading.Lock()
        self.metrics = {
            "hits": 0,
            "misses": 0,
            "evictions": 0,
            "size": 0
        }

    def get(self, key: str) -> Optional[Any]:
        """Get value from L1 cache"""
        with self.lock:
            if key in self.cache:
                entry = self.cache[key]

                if entry.is_expired():
                    del self.cache[key]
                    self.metrics["misses"] += 1
                    return None

                # Update access information
                entry.update_access()

                # Move to end for LRU
                if self.strategy == CacheStrategy.LRU:
                    self.cache.move_to_end(key)

                self.metrics["hits"] += 1
                return entry.value

            self.metrics["misses"] += 1
            return None

    def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
        """Set value in L1 cache"""
        with self.lock:
            # Calculate size
            size = len(pickle.dumps(value))

            entry = CacheEntry(
                key=key,
                value=value,
                created_at=datetime.utcnow(),
                accessed_at=datetime.utcnow(),
                ttl=ttl,
                size=size
            )

            # Remove existing entry if present
            if key in self.cache:
                del self.cache[key]

            # Add new entry
            self.cache[key] = entry

            # Evict if over capacity
            while len(self.cache) > self.max_size:
                self._evict_entry()

    def delete(self, key: str) -> bool:
        """Delete key from L1 cache"""
        with self.lock:
            if key in self.cache:
                del self.cache[key]
                return True
            return False

    def clear(self) -> None:
        """Clear L1 cache"""
        with self.lock:
            self.cache.clear()

    def _evict_entry(self) -> None:
        """Evict entry based on strategy"""
        if not self.cache:
            return

        if self.strategy == CacheStrategy.LRU:
            # Remove least recently used
            self.cache.popitem(last=False)
        elif self.strategy == CacheStrategy.LFU:
            # Remove least frequently used
            lfu_key = min(self.cache.keys(), key=lambda k: self.cache[k].access_count)
            del self.cache[lfu_key]

        self.metrics["evictions"] += 1

    def get_metrics(self) -> Dict[str, Any]:
        """Get cache metrics"""
        total_requests = self.metrics["hits"] + self.metrics["misses"]
        hit_rate = (self.metrics["hits"] / total_requests * 100) if total_requests > 0 else 0

        return {
            **self.metrics,
            "hit_rate": hit_rate,
            "current_size": len(self.cache),
            "max_size": self.max_size
        }

class L2Cache:
    """L2 Redis cache implementation"""

    def __init__(self, redis_pool: RedisConnectionPool, key_prefix: str = "cache:"):
        self.redis_pool = redis_pool
        self.key_prefix = key_prefix
        self.client = redis.Redis(connection_pool=redis_pool)
        self.metrics = {
            "hits": 0,
            "misses": 0,
            "errors": 0
        }

    def get(self, key: str) -> Optional[Any]:
        """Get value from L2 cache"""
        try:
            full_key = f"{self.key_prefix}{key}"
            value = self.client.get(full_key)

            if value is not None:
                self.metrics["hits"] += 1
                return pickle.loads(value)
            else:
                self.metrics["misses"] += 1
                return None

        except Exception as e:
            self.metrics["errors"] += 1
            logger.error(f"L2 cache get error: {e}")
            return None

    def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in L2 cache"""
        try:
            full_key = f"{self.key_prefix}{key}"
            serialized_value = pickle.dumps(value)

            if ttl:
                self.client.setex(full_key, ttl, serialized_value)
            else:
                self.client.set(full_key, serialized_value)

            return True

        except Exception as e:
            self.metrics["errors"] += 1
            logger.error(f"L2 cache set error: {e}")
            return False

    def delete(self, key: str) -> bool:
        """Delete key from L2 cache"""
        try:
            full_key = f"{self.key_prefix}{key}"
            result = self.client.delete(full_key)
            return result > 0

        except Exception as e:
            self.metrics["errors"] += 1
            logger.error(f"L2 cache delete error: {e}")
            return False

    def get_metrics(self) -> Dict[str, Any]:
        """Get cache metrics"""
        total_requests = self.metrics["hits"] + self.metrics["misses"]
        hit_rate = (self.metrics["hits"] / total_requests * 100) if total_requests > 0 else 0

        return {
            **self.metrics,
            "hit_rate": hit_rate
        }

class AsyncL2Cache:
    """Async L2 Redis cache implementation"""

    def __init__(self, redis_pool, key_prefix: str = "cache:"):
        self.redis_pool = redis_pool
        self.key_prefix = key_prefix
        self.metrics = {
            "hits": 0,
            "misses": 0,
            "errors": 0
        }

    async def get(self, key: str) -> Optional[Any]:
        """Get value from async L2 cache"""
        try:
            full_key = f"{self.key_prefix}{key}"
            value = await self.redis_pool.get(full_key)

            if value is not None:
                self.metrics["hits"] += 1
                return pickle.loads(value)
            else:
                self.metrics["misses"] += 1
                return None

        except Exception as e:
            self.metrics["errors"] += 1
            logger.error(f"Async L2 cache get error: {e}")
            return None

    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in async L2 cache"""
        try:
            full_key = f"{self.key_prefix}{key}"
            serialized_value = pickle.dumps(value)

            if ttl:
                await self.redis_pool.setex(full_key, ttl, serialized_value)
            else:
                await self.redis_pool.set(full_key, serialized_value)

            return True

        except Exception as e:
            self.metrics["errors"] += 1
            logger.error(f"Async L2 cache set error: {e}")
            return False

    async def delete(self, key: str) -> bool:
        """Delete key from async L2 cache"""
        try:
            full_key = f"{self.key_prefix}{key}"
            result = await self.redis_pool.delete(full_key)
            return result > 0

        except Exception as e:
            self.metrics["errors"] += 1
            logger.error(f"Async L2 cache delete error: {e}")
            return False

class MultiLevelCache:
    """Multi-level cache implementation"""

    def __init__(self, l1_cache: L1Cache, l2_cache: L2Cache):
        self.l1_cache = l1_cache
        self.l2_cache = l2_cache
        self.metrics = {
            "l1_hits": 0,
            "l2_hits": 0,
            "misses": 0,
            "writes": 0
        }

    def get(self, key: str) -> Optional[Any]:
        """Get value from multi-level cache"""
        # Try L1 cache first
        value = self.l1_cache.get(key)
        if value is not None:
            self.metrics["l1_hits"] += 1
            return value

        # Try L2 cache
        value = self.l2_cache.get(key)
        if value is not None:
            self.metrics["l2_hits"] += 1
            # Populate L1 cache
            self.l1_cache.set(key, value)
            return value

        self.metrics["misses"] += 1
        return None

    def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
        """Set value in multi-level cache"""
        # Set in both levels
        self.l1_cache.set(key, value, ttl)
        self.l2_cache.set(key, value, ttl)
        self.metrics["writes"] += 1

    def delete(self, key: str) -> None:
        """Delete key from multi-level cache"""
        self.l1_cache.delete(key)
        self.l2_cache.delete(key)

    def get_metrics(self) -> Dict[str, Any]:
        """Get multi-level cache metrics"""
        total_requests = self.metrics["l1_hits"] + self.metrics["l2_hits"] + self.metrics["misses"]
        l1_hit_rate = (self.metrics["l1_hits"] / total_requests * 100) if total_requests > 0 else 0
        l2_hit_rate = (self.metrics["l2_hits"] / total_requests * 100) if total_requests > 0 else 0
        overall_hit_rate = ((self.metrics["l1_hits"] + self.metrics["l2_hits"]) / total_requests * 100) if total_requests > 0 else 0

        return {
            **self.metrics,
            "l1_hit_rate": l1_hit_rate,
            "l2_hit_rate": l2_hit_rate,
            "overall_hit_rate": overall_hit_rate,
            "l1_metrics": self.l1_cache.get_metrics(),
            "l2_metrics": self.l2_cache.get_metrics()
        }

class CacheDecorator:
    """Cache decorator for function results"""

    def __init__(self, cache: MultiLevelCache, ttl: Optional[int] = None, key_prefix: str = ""):
        self.cache = cache
        self.ttl = ttl
        self.key_prefix = key_prefix

    def __call__(self, func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Generate cache key
            cache_key = self._generate_cache_key(func.__name__, args, kwargs)

            # Try to get from cache
            cached_result = self.cache.get(cache_key)
            if cached_result is not None:
                return cached_result

            # Execute function
            result = func(*args, **kwargs)

            # Cache result
            self.cache.set(cache_key, result, self.ttl)

            return result

        return wrapper

    def _generate_cache_key(self, func_name: str, args: tuple, kwargs: dict) -> str:
        """Generate cache key from function arguments"""
        key_data = {
            "func_name": func_name,
            "args": args,
            "kwargs": sorted(kwargs.items())
        }
        key_string = json.dumps(key_data, sort_keys=True)
        return f"{self.key_prefix}{hashlib.md5(key_string.encode()).hexdigest()}"

class CacheInvalidation:
    """Cache invalidation strategies"""

    def __init__(self, cache: MultiLevelCache):
        self.cache = cache
        self.invalidation_patterns = {}

    def add_pattern(self, pattern: str, keys: List[str]) -> None:
        """Add invalidation pattern"""
        self.invalidation_patterns[pattern] = keys

    def invalidate_by_pattern(self, pattern: str) -> int:
        """Invalidate keys matching pattern"""
        if pattern not in self.invalidation_patterns:
            return 0

        invalidated_count = 0
        for key in self.invalidation_patterns[pattern]:
            if self.cache.l1_cache.delete(key):
                invalidated_count += 1
            if self.cache.l2_cache.delete(key):
                invalidated_count += 1

        return invalidated_count

    def invalidate_by_prefix(self, prefix: str) -> int:
        """Invalidate keys with prefix"""
        invalidated_count = 0

        # Invalidate L1 cache
        with self.cache.l1_cache.lock:
            keys_to_delete = [key for key in self.cache.l1_cache.cache.keys() if key.startswith(prefix)]
            for key in keys_to_delete:
                if self.cache.l1_cache.delete(key):
                    invalidated_count += 1

        # Invalidate L2 cache
        try:
            # This would use Redis SCAN in real implementation
            pass
        except Exception as e:
            logger.error(f"Error invalidating L2 cache by prefix: {e}")

        return invalidated_count

# Usage examples
def example_multi_level_caching():
    """Example multi-level caching usage"""
    # Create L1 cache
    l1_cache = L1Cache(max_size=100, strategy=CacheStrategy.LRU)

    # Create Redis connection pool
    redis_pool = RedisConnectionPool(host='localhost', port=6379, db=0)

    # Create L2 cache
    l2_cache = L2Cache(redis_pool, key_prefix="app:")

    # Create multi-level cache
    multi_cache = MultiLevelCache(l1_cache, l2_cache)

    # Set values
    multi_cache.set("user:1", {"id": 1, "name": "John"}, ttl=3600)
    multi_cache.set("user:2", {"id": 2, "name": "Jane"}, ttl=3600)

    # Get values
    user1 = multi_cache.get("user:1")
    print(f"Retrieved user1: {user1}")

    # Get cache metrics
    metrics = multi_cache.get_metrics()
    print(f"Cache metrics: {metrics}")

    # Cache decorator
    cache_decorator = CacheDecorator(multi_cache, ttl=300)

    @cache_decorator
    def expensive_calculation(n: int) -> int:
        time.sleep(0.1)  # Simulate expensive operation
        return n * n

    # Test cached function
    result1 = expensive_calculation(10)
    result2 = expensive_calculation(10)  # Should be cached
    print(f"Calculation results: {result1}, {result2}")

    # Cache invalidation
    invalidation = CacheInvalidation(multi_cache)
    invalidation.add_pattern("users", ["user:1", "user:2"])
    invalidated = invalidation.invalidate_by_pattern("users")
    print(f"Invalidated {invalidated} cache entries")

Distributed Caching

# python/02-distributed-caching.py

"""
Distributed caching patterns and consistency strategies
"""

from typing import List, Dict, Any, Optional, Union, Tuple
import time
import json
import hashlib
import asyncio
from datetime import datetime, timedelta
import logging
from dataclasses import dataclass
from enum import Enum
import redis
from redis import ConnectionPool as RedisConnectionPool
import aioredis
from consistent_hash import ConsistentHash
import threading
from concurrent.futures import ThreadPoolExecutor

logger = logging.getLogger(__name__)

class ConsistencyLevel(Enum):
    """Consistency level enumeration"""
    EVENTUAL = "eventual"
    STRONG = "strong"
    WEAK = "weak"

class DistributedCache:
    """Distributed cache implementation"""

    def __init__(self, nodes: List[str], consistency_level: ConsistencyLevel = ConsistencyLevel.EVENTUAL):
        self.nodes = nodes
        self.consistency_level = consistency_level
        self.consistent_hash = ConsistentHash(nodes)
        self.redis_pools = {}
        self.initialize_connections()
        self.metrics = {
            "hits": 0,
            "misses": 0,
            "errors": 0,
            "replications": 0
        }

    def initialize_connections(self) -> None:
        """Initialize connections to all nodes"""
        for node in self.nodes:
            try:
                pool = RedisConnectionPool.from_url(f"redis://{node}")
                self.redis_pools[node] = redis.Redis(connection_pool=pool)
            except Exception as e:
                logger.error(f"Failed to connect to node {node}: {e}")

    def get_node(self, key: str) -> str:
        """Get node for key using consistent hashing"""
        return self.consistent_hash.get_node(key)

    def get(self, key: str) -> Optional[Any]:
        """Get value from distributed cache"""
        try:
            # Get primary node
            primary_node = self.get_node(key)

            # Try primary node first
            value = self._get_from_node(primary_node, key)
            if value is not None:
                self.metrics["hits"] += 1
                return value

            # Try other nodes for eventual consistency
            if self.consistency_level == ConsistencyLevel.EVENTUAL:
                for node in self.nodes:
                    if node != primary_node:
                        value = self._get_from_node(node, key)
                        if value is not None:
                            # Update primary node
                            self._set_in_node(primary_node, key, value)
                            self.metrics["hits"] += 1
                            return value

            self.metrics["misses"] += 1
            return None

        except Exception as e:
            self.metrics["errors"] += 1
            logger.error(f"Distributed cache get error: {e}")
            return None

    def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in distributed cache"""
        try:
            # Get primary node
            primary_node = self.get_node(key)

            # Set in primary node
            success = self._set_in_node(primary_node, key, value, ttl)
            if not success:
                return False

            # Replicate to other nodes based on consistency level
            if self.consistency_level == ConsistencyLevel.STRONG:
                # Synchronous replication
                for node in self.nodes:
                    if node != primary_node:
                        self._set_in_node(node, key, value, ttl)
                        self.metrics["replications"] += 1
            elif self.consistency_level == ConsistencyLevel.EVENTUAL:
                # Asynchronous replication
                self._async_replicate(key, value, ttl, primary_node)

            return True

        except Exception as e:
            self.metrics["errors"] += 1
            logger.error(f"Distributed cache set error: {e}")
            return False

    def delete(self, key: str) -> bool:
        """Delete key from distributed cache"""
        try:
            # Get primary node
            primary_node = self.get_node(key)

            # Delete from primary node
            success = self._delete_from_node(primary_node, key)

            # Delete from other nodes
            for node in self.nodes:
                if node != primary_node:
                    self._delete_from_node(node, key)

            return success

        except Exception as e:
            self.metrics["errors"] += 1
            logger.error(f"Distributed cache delete error: {e}")
            return False

    def _get_from_node(self, node: str, key: str) -> Optional[Any]:
        """Get value from specific node"""
        try:
            if node in self.redis_pools:
                value = self.redis_pools[node].get(key)
                return json.loads(value) if value else None
        except Exception as e:
            logger.error(f"Error getting from node {node}: {e}")
        return None

    def _set_in_node(self, node: str, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in specific node"""
        try:
            if node in self.redis_pools:
                serialized_value = json.dumps(value)
                if ttl:
                    self.redis_pools[node].setex(key, ttl, serialized_value)
                else:
                    self.redis_pools[node].set(key, serialized_value)
                return True
        except Exception as e:
            logger.error(f"Error setting in node {node}: {e}")
        return False

    def _delete_from_node(self, node: str, key: str) -> bool:
        """Delete key from specific node"""
        try:
            if node in self.redis_pools:
                result = self.redis_pools[node].delete(key)
                return result > 0
        except Exception as e:
            logger.error(f"Error deleting from node {node}: {e}")
        return False

    def _async_replicate(self, key: str, value: Any, ttl: Optional[int], exclude_node: str) -> None:
        """Asynchronously replicate to other nodes"""
        def replicate():
            for node in self.nodes:
                if node != exclude_node:
                    self._set_in_node(node, key, value, ttl)
                    self.metrics["replications"] += 1

        # Run in background thread
        thread = threading.Thread(target=replicate)
        thread.daemon = True
        thread.start()

    def get_metrics(self) -> Dict[str, Any]:
        """Get distributed cache metrics"""
        total_requests = self.metrics["hits"] + self.metrics["misses"]
        hit_rate = (self.metrics["hits"] / total_requests * 100) if total_requests > 0 else 0

        return {
            **self.metrics,
            "hit_rate": hit_rate,
            "nodes": len(self.nodes),
            "consistency_level": self.consistency_level.value
        }

class CacheSharding:
    """Cache sharding implementation"""

    def __init__(self, shards: List[str], shard_key_func: Optional[Callable] = None):
        self.shards = shards
        self.shard_key_func = shard_key_func or self._default_shard_key
        self.shard_connections = {}
        self.initialize_shards()

    def initialize_shards(self) -> None:
        """Initialize connections to all shards"""
        for shard in self.shards:
            try:
                pool = RedisConnectionPool.from_url(f"redis://{shard}")
                self.shard_connections[shard] = redis.Redis(connection_pool=pool)
            except Exception as e:
                logger.error(f"Failed to connect to shard {shard}: {e}")

    def _default_shard_key(self, key: str) -> str:
        """Default shard key function"""
        hash_value = int(hashlib.md5(key.encode()).hexdigest(), 16)
        return self.shards[hash_value % len(self.shards)]

    def get_shard(self, key: str) -> str:
        """Get shard for key"""
        return self.shard_key_func(key)

    def get(self, key: str) -> Optional[Any]:
        """Get value from sharded cache"""
        shard = self.get_shard(key)
        try:
            if shard in self.shard_connections:
                value = self.shard_connections[shard].get(key)
                return json.loads(value) if value else None
        except Exception as e:
            logger.error(f"Error getting from shard {shard}: {e}")
        return None

    def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in sharded cache"""
        shard = self.get_shard(key)
        try:
            if shard in self.shard_connections:
                serialized_value = json.dumps(value)
                if ttl:
                    self.shard_connections[shard].setex(key, ttl, serialized_value)
                else:
                    self.shard_connections[shard].set(key, serialized_value)
                return True
        except Exception as e:
            logger.error(f"Error setting in shard {shard}: {e}")
        return False

    def delete(self, key: str) -> bool:
        """Delete key from sharded cache"""
        shard = self.get_shard(key)
        try:
            if shard in self.shard_connections:
                result = self.shard_connections[shard].delete(key)
                return result > 0
        except Exception as e:
            logger.error(f"Error deleting from shard {shard}: {e}")
        return False

class CacheWarming:
    """Cache warming strategies"""

    def __init__(self, cache: Union[MultiLevelCache, DistributedCache]):
        self.cache = cache
        self.warming_tasks = []
        self.warming_metrics = {
            "warmed_keys": 0,
            "failed_warms": 0,
            "warming_time": 0.0
        }

    def add_warming_task(self, key: str, value_func: Callable, ttl: Optional[int] = None) -> None:
        """Add cache warming task"""
        self.warming_tasks.append({
            "key": key,
            "value_func": value_func,
            "ttl": ttl
        })

    def warm_cache(self) -> Dict[str, Any]:
        """Warm cache with all tasks"""
        start_time = time.time()
        warmed_keys = 0
        failed_warms = 0

        for task in self.warming_tasks:
            try:
                # Execute value function
                value = task["value_func"]()

                # Set in cache
                self.cache.set(task["key"], value, task["ttl"])
                warmed_keys += 1

            except Exception as e:
                logger.error(f"Failed to warm cache for key {task['key']}: {e}")
                failed_warms += 1

        warming_time = time.time() - start_time
        self.warming_metrics["warmed_keys"] += warmed_keys
        self.warming_metrics["failed_warms"] += failed_warms
        self.warming_metrics["warming_time"] += warming_time

        return {
            "warmed_keys": warmed_keys,
            "failed_warms": failed_warms,
            "warming_time": warming_time
        }

    def get_warming_metrics(self) -> Dict[str, Any]:
        """Get cache warming metrics"""
        return self.warming_metrics.copy()

class CacheMonitoring:
    """Cache monitoring and alerting"""

    def __init__(self, cache: Union[MultiLevelCache, DistributedCache]):
        self.cache = cache
        self.monitoring_metrics = {}
        self.alerts = []

    def check_cache_health(self) -> Dict[str, Any]:
        """Check cache health"""
        health_status = {
            "healthy": True,
            "issues": [],
            "timestamp": datetime.utcnow().isoformat()
        }

        # Check hit rate
        metrics = self.cache.get_metrics()
        hit_rate = metrics.get("overall_hit_rate", 0)

        if hit_rate < 50:  # Low hit rate threshold
            health_status["healthy"] = False
            health_status["issues"].append(f"Low hit rate: {hit_rate:.2f}%")

        # Check error rate
        if "errors" in metrics:
            error_rate = metrics["errors"] / max(metrics.get("hits", 0) + metrics.get("misses", 0), 1) * 100
            if error_rate > 5:  # High error rate threshold
                health_status["healthy"] = False
                health_status["issues"].append(f"High error rate: {error_rate:.2f}%")

        return health_status

    def add_alert(self, condition: Callable, message: str) -> None:
        """Add monitoring alert"""
        self.alerts.append({
            "condition": condition,
            "message": message
        })

    def check_alerts(self) -> List[str]:
        """Check all alerts"""
        triggered_alerts = []

        for alert in self.alerts:
            try:
                if alert["condition"]():
                    triggered_alerts.append(alert["message"])
            except Exception as e:
                logger.error(f"Error checking alert: {e}")

        return triggered_alerts

# Usage examples
def example_distributed_caching():
    """Example distributed caching usage"""
    # Create distributed cache
    nodes = ["redis-node-1:6379", "redis-node-2:6379", "redis-node-3:6379"]
    distributed_cache = DistributedCache(nodes, ConsistencyLevel.EVENTUAL)

    # Set values
    distributed_cache.set("user:1", {"id": 1, "name": "John"}, ttl=3600)
    distributed_cache.set("user:2", {"id": 2, "name": "Jane"}, ttl=3600)

    # Get values
    user1 = distributed_cache.get("user:1")
    print(f"Retrieved user1: {user1}")

    # Get metrics
    metrics = distributed_cache.get_metrics()
    print(f"Distributed cache metrics: {metrics}")

    # Cache sharding
    shards = ["redis-shard-1:6379", "redis-shard-2:6379", "redis-shard-3:6379"]
    sharded_cache = CacheSharding(shards)

    # Use sharded cache
    sharded_cache.set("product:1", {"id": 1, "name": "Laptop"}, ttl=1800)
    product = sharded_cache.get("product:1")
    print(f"Retrieved product: {product}")

    # Cache warming
    warming = CacheWarming(distributed_cache)

    # Add warming tasks
    warming.add_warming_task("popular_users", lambda: [{"id": i, "name": f"User{i}"} for i in range(1, 11)])
    warming.add_warming_task("config", lambda: {"theme": "dark", "language": "en"})

    # Warm cache
    warming_result = warming.warm_cache()
    print(f"Cache warming result: {warming_result}")

    # Cache monitoring
    monitoring = CacheMonitoring(distributed_cache)

    # Add alerts
    monitoring.add_alert(
        lambda: distributed_cache.get_metrics().get("overall_hit_rate", 0) < 50,
        "Low cache hit rate detected"
    )

    # Check health
    health = monitoring.check_cache_health()
    print(f"Cache health: {health}")

    # Check alerts
    alerts = monitoring.check_alerts()
    if alerts:
        print(f"Triggered alerts: {alerts}")

TL;DR Runbook

Quick Start

# 1. Multi-level caching
l1_cache = L1Cache(max_size=1000, strategy=CacheStrategy.LRU)
l2_cache = L2Cache(redis_pool, key_prefix="app:")
multi_cache = MultiLevelCache(l1_cache, l2_cache)

# 2. Cache decorator
cache_decorator = CacheDecorator(multi_cache, ttl=300)
@cache_decorator
def expensive_function(n: int) -> int:
    return n * n

# 3. Distributed caching
distributed_cache = DistributedCache(nodes, ConsistencyLevel.EVENTUAL)
distributed_cache.set("key", "value", ttl=3600)

# 4. Cache sharding
sharded_cache = CacheSharding(shards)
sharded_cache.set("key", "value")

# 5. Cache warming
warming = CacheWarming(multi_cache)
warming.add_warming_task("data", lambda: fetch_data())
warming.warm_cache()

Essential Patterns

# Complete caching setup
def setup_caching_strategies():
    """Setup complete caching strategies environment"""

    # L1 cache
    l1_cache = L1Cache(max_size=1000, strategy=CacheStrategy.LRU)

    # L2 cache
    redis_pool = RedisConnectionPool(host='localhost', port=6379)
    l2_cache = L2Cache(redis_pool, key_prefix="app:")

    # Multi-level cache
    multi_cache = MultiLevelCache(l1_cache, l2_cache)

    # Distributed cache
    distributed_cache = DistributedCache(nodes, ConsistencyLevel.EVENTUAL)

    # Cache sharding
    sharded_cache = CacheSharding(shards)

    # Cache warming
    warming = CacheWarming(multi_cache)

    # Cache monitoring
    monitoring = CacheMonitoring(multi_cache)

    print("Caching strategies setup complete!")

This guide provides the complete machinery for Python caching strategies. Each pattern includes implementation examples, caching strategies, and real-world usage patterns for enterprise cache management.