Building Valo: Redis Cache Adapter — Decorator Pattern, TTL, and Graceful Degradation
API calls take seconds. Database queries take milliseconds. Cached data takes microseconds.
Today we implemented Valo's Redis cache adapter with decorator pattern, automatic serialization, TTL support, connection pooling, and graceful degradation—production-ready caching infrastructure.
The Problem: Expensive Operations Without Memoization
Sports intelligence requires data aggregation from multiple sources. Without caching, every request repeats expensive operations:
- API calls (~2 seconds each)
- Database queries (~50ms each)
- ML inference (~500ms)
The pain: High API costs, slow UX, wasted compute, poor scalability.
Our Solution: Two-Level API
Level 1: @cache_result Decorator (Simple Cases)
@cache_result(ttl=600, key_prefix="matches")
def get_match_prediction(match_id: str) -> dict:
# Expensive operations only on cache miss
return prediction
# First call: cache miss, 5 seconds
# Subsequent calls: cache hit, <5ms!Level 2: RedisCache Class (Advanced Control)
cache = RedisCache(host='redis.sipap.io', default_ttl=300)
# Try cache first
roster = cache.get(cache_key)
if roster is None:
roster = api.fetch_roster(team_id)
cache.set(cache_key, roster, ttl=3600)Implementation: RedisCache Class
Design decisions:
- Connection Pooling: Reuses connections instead of creating new ones
- Dependency Injection: Client parameter enables testing with mocks
- Configurable TTL: Defaults to 5 minutes, overridable per operation
- Type Hints: Full mypy strict mode compliance
Automatic JSON Serialization
Why JSON?
- Supports complex nested data structures
- Human-readable (easy debugging)
- Language-agnostic (other services can read)
- Standard library support
Error handling:
- Wraps all Redis errors in
CacheError - Treats invalid JSON as cache miss (graceful degradation)
Cache Key Generation Strategy
Solution:
- Serialize arguments to JSON (sorted for determinism)
- Hash with MD5 (collision-resistant)
- Combine function name + hash
- Optional namespace prefix
Properties:
- Deterministic: Same arguments → same key
- Collision-resistant: MD5 provides 128-bit hash space
- Namespace-aware: Prevents collisions across functions
- Human-readable: Key structure is
prefix:function:hash
Graceful Degradation: The Critical Feature
Scenario: Redis server goes down during peak traffic.
With fail_silently=True (default): Function still works, just slower.
Trade-offs:
fail_silently=True: Higher availability, slower performance when Redis downfail_silently=False: Fail fast, explicit error handling required
Connection Pooling: Production Performance
Benefits:
- Reuse: Connections persist across requests
- Thread-safe: Pool handles concurrent access
- Configurable: Adjust pool size for workload
- Automatic: Pool manages connection lifecycle
Benchmarks:
- Without pooling: ~10ms overhead per operation
- With pooling: <1ms overhead per operation
Performance Characteristics
| Operation | Without Cache | With Cache | Improvement |
|---|---|---|---|
| API call | 2,000ms | 5ms | 400x faster |
| Database query | 50ms | 5ms | 10x faster |
| ML prediction | 500ms | 5ms | 100x faster |
Quality Metrics
- 156 tests passing (24 new cache tests, 132 existing)
- 87% overall coverage (cache adapter: 86%)
- Zero mypy errors (strict mode)
- Zero ruff errors
Fast. Resilient. Production-ready.