Ridha Tech
Get Started
← Back to Blog
Technical Deep Dive

Building Valo: Redis Cache Adapter — Decorator Pattern, TTL, and Graceful Degradation

June 8, 202612 min read

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:

  1. Connection Pooling: Reuses connections instead of creating new ones
  2. Dependency Injection: Client parameter enables testing with mocks
  3. Configurable TTL: Defaults to 5 minutes, overridable per operation
  4. 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:

  1. Serialize arguments to JSON (sorted for determinism)
  2. Hash with MD5 (collision-resistant)
  3. Combine function name + hash
  4. 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 down
  • fail_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

OperationWithout CacheWith CacheImprovement
API call2,000ms5ms400x faster
Database query50ms5ms10x faster
ML prediction500ms5ms100x 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.