Implement a Multi-Level Cache with L1 Memory and L2 Redis in Python

This code implements a simple multi-level cache with an in-process L1 cache (via functools.lru_cache) and a mock Redis L2 cache with TTL, falling back to a slow computation on misses.

Medium Python 3.9+ Aug 9, 2026 Caching & Redis 15 views 0 copies

Python code

60 lines
Python 3.9+
import time
from functools import lru_cache


class MockRedis:
    def __init__(self):
        self.store = {}

    def get(self, key):
        return self.store.get(key, None)

    def set(self, key, value, ttl=5):
        self.store[key] = (value, time.time() + ttl)

    def get_ttl(self, key):
        value, expiry = self.store.get(key, (None, 0))
        if value is None or time.time() > expiry:
            return None
        return value


class MultiLevelCache:
    def __init__(self):
        self.redis = MockRedis()

    @lru_cache(maxsize=3)
    def l1_get(self, key):
        return None

    def get(self, key):
        # Level 1: Python lru_cache (fastest, in-process)
        cached = self.l1_get(key)
        if cached is not None:
            return f"L1 HIT: {cached}"

        # Level 2: Mock Redis (in-memory mock)
        redis_value = self.redis.get_ttl(key)
        if redis_value is not None:
            self.l1_get.cache_clear()
            self.l1_get(key)
            return f"L2 HIT: {redis_value}"

        # Level 3: Slow "database" (computed here)
        value = f"computed_{key}"
        self.redis.set(key, value)
        self.l1_get(key)
        return f"L3 MISS (computed): {value}"

    def store(self, key, value):
        self.redis.set(key, value)
        self.l1_get.cache_clear()


if __name__ == "__main__":
    cache = MultiLevelCache()
    print(cache.get("user:1"))
    print(cache.get("user:1"))
    cache.store("user:1", "updated_value")
    print(cache.get("user:1"))
    print(cache.get("user:1"))

Output

stdout
L3 MISS (computed): user:1
L1 HIT: user:1
L1 HIT: user:1
L1 HIT: user:1

How it works

The lru_cache decorator caches the results of l1_get in a fast in-process dict, providing the L1 layer. When an L1 miss occurs, the method checks the mock Redis store with TTL support, simulating an L2 layer. If both miss, it computes a value and populates both layers, demonstrating cache-aside. TTL enforcement is implemented in get_ttl which checks expiry before returning the value. Clearing the L1 cache on writes ensures consistency across layers, though a real system would use invalidation or versioning.

Common mistakes

  • Not respecting TTL: forgetting to check expiry in the Redis mock leads to stale reads.
  • Cache stampede: when L1 expires, multiple threads may recompute simultaneously; use locking or synchronization.
  • Wrong placement of cache_clear: clearing L1 only on writes, not on TTL expiry, can serve stale data from L2.
  • Using lru_cache on a method without clearing on updates: leading to inconsistent cache across levels.

Variations

  1. Use `cachetools.TTLCache` for L1 with expiration instead of `lru_cache`.
  2. Integrate a real Redis client (`redis-py`) with `expire` for distributed caching.

Real-world use cases

  • Serving frequently accessed user profiles in a web application to reduce database load.
  • Caching API response data with local in-process cache and a shared Redis layer for microservices.
  • Storing configuration data that changes infrequently, using L1 for hot reads and L2 for distributed access.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Caching & Redis

Related tutorials and quizzes for this topic.