Cache Penetration Null Object Mock in Python
Implement a cache that stores a null marker on misses to prevent repeated database hits, reducing cache penetration.
Python code
47 linesimport time
from collections import defaultdict
from typing import Any, Optional
class Cache:
def __init__(self):
self.store: dict[str, Any] = {}
self.ttl: dict[str, float] = {}
self.null_marker = object()
def get(self, key: str, ttl: int = 60, fallback:
Any = None) -> Any:
now = time.time()
if key in self.ttl and now > self.ttl[key]:
self.store.pop(key, None)
self.ttl.pop(key, None)
if key in self.store:
return self.store[key]
# Simulate cache penetration: miss returns None
if fallback is not None:
# Store null object to avoid repeated DB hits
self.store[key] = self.null_marker
self.ttl[key] = now + ttl
return self.store[key]
return None
def set(self, key: str, value: Any, ttl: int = 60) -> None:
self.store[key] = value
self.ttl[key] = time.time() + ttl
if __name__ == "__main__":
cache = Cache()
# Miss for "user:1", use fallback -> store null object
result1 = cache.get("user:1", fallback=None)
# Subsequent hit: matches null object, but we treat as miss
result2 = cache.get("user:1")
# Real value set
cache.set("user:1", {"name": "Alice"})
result3 = cache.get("user:1")
print(f"First call: {result1}")
print(f"Second call (should be null object): {result2 is cache.null_marker}")
print(f"After set: {result3}")
Output
First call: None
Second call (should be null object): True
After set: {'name': 'Alice'}
How it works
The Cache class uses a dict to store values and a separate dict for TTL timestamps. When the TTL expires, the key is evicted lazily during get. On a miss, it stores a unique null_marker object to signal a negative result, preventing repeated expensive DB lookups for nonexistent keys. The fallback parameter lets callers distinguish between a genuine miss and a cached negative. A subsequent set overwrites the marker with real data, and get returns it immediately.
Common mistakes
- Storing `None` directly instead of a unique sentinel, which conflates a cached miss with a nonexistent key.
- Forgetting to set TTL on the negative entry, causing it to persist forever.
- Checking `key in self.store` without handling expiration first, returning stale data.
Variations
- Use a dedicated `cache.set_null(key, ttl)` method to make the intent explicit.
- Store the TTL as a tuple `(value, expiry)` inside one dict instead of two separate structures.
Real-world use cases
- Preventing repeated database lookups for user IDs that don't exist in a high-traffic service.
- Avoiding hot-key contention when an external API frequently returns 404 for a specific resource.
- Short-circuiting expensive ML feature computations for inputs known to be invalid or missing.
Sponsored
More from Caching & Redis
- Cache Asides in Python with a Read-Through Loader easy
- Cache Data in Redis with Python easy
- Cache Stampede Prevention with SingleFlight in Python medium
- Cache Warming with Python: Preload Hot Keys easy
- Coalescing duplicate in-flight requests: one shared result for concurrent callers hard
- Consistent Hashing Cache Shard in Python medium
Keep learning
Related tutorials and quizzes for this topic.