Requirements
Section titled “Requirements”| Package | Required | Purpose |
|---|---|---|
oridecon | Yes | Core framework |
oridecon-contracts | Yes | Protocol definitions |
redis | Recommended | Redis cache backend |
pymemcache | Optional | Memcached cache backend |
Overview
Section titled “Overview”oridecon-cache provides a unified caching API across multiple backends. It handles the common caching patterns so you don’t have to — stampede protection, TTL management, serialization, tag-based invalidation, and health checks.
Mental Model
Section titled “Mental Model”Application Code │ ▼ CacheService ← unified API (get/set/delete/delete_pattern) │ ├── StampedeProtectedCache ← lock-based stampede prevention │ ▼ CacheBackendProtocol ← backend abstraction (Result-based) │ ├── MemoryCacheBackend (in-process, no deps) ├── RedisCacheBackend (requires redis-py) └── MemcachedCacheBackend (requires pymemcache)Core Concepts
Section titled “Core Concepts”Backend Abstraction
Section titled “Backend Abstraction”All backends implement CacheBackendProtocol from oridecon.contracts.infra.cache. The protocol returns Result[T, CacheError] for every operation:
from oridecon.contracts.infra.cache import CacheBackendProtocolfrom oridecon.result import Ok, Err
# Backend returns Result — CacheService unwraps it internallyresult = await backend.get("my-key")if result.is_ok(): value = result.unwrap()| Backend | Extra | Storage |
|---|---|---|
MemoryCacheBackend | None | In-process dict |
RedisCacheBackend | oridecon-cache[redis] | Redis server |
MemcachedCacheBackend | oridecon-cache[memcached] | Memcached server |
CacheService (High-Level API)
Section titled “CacheService (High-Level API)”CacheService wraps the backend and provides ergonomic access:
from oridecon.cache import CacheService
# Resolved from the container — inject via constructorawait cache.set("user:42", {"name": "Alice"}, ttl=300)value = await cache.get("user:42") # → {"name": "Alice"}await cache.delete("user:42") # → Truecount = await cache.delete_pattern("user:*") # → number of keysCacheService.get() returns the raw value or default (not Result). Errors are logged and return the default — the service prefers graceful degradation over crashing.
Named Backends
Section titled “Named Backends”Configure multiple backends with different names in CacheConfig.backends. The first default: true backend is the default:
cache: backends: - name: "hot" type: memory default: true - name: "persistent" type: redis host: localhost port: 6379Resolve a specific backend:
hot_cache = await container.resolve(CacheService, name="hot")persistent_cache = await container.resolve(CacheService, name="persistent")Stampede Protection
Section titled “Stampede Protection”When service.enable_protection is True (default), CacheService uses lock-based stampede protection. Only one process recomputes the value while others wait:
cache.service: enable_protection: true protection_lock_ttl: 30 protection_max_wait: 10.0Tag-Based Invalidation
Section titled “Tag-Based Invalidation”Tag cache entries so you can invalidate groups of keys:
from oridecon.cache import CacheService
await cache.set("article:1", data, tags=["articles", "breaking"])await cache.set("article:2", data, tags=["articles"])
# Invalidate all articlesawait cache.invalidate_tags(["articles"])
# The next get() for article:1 returns NoneSerialization
Section titled “Serialization”CacheService serializes values to JSON by default. JSON is the only built-in safe serializer — pickle is not available:
cache.service: default_serializer: "json" # defaultAvailable serializers: JSONSerializer (default), MsgPackSerializer (optional, compact binary), CompressingSerializer (wraps another serializer with gzip/zlib). Objects reconstructed through @cacheable type tags are resolved only against the deny-by-default oridecon.cache.serialization.DEFAULT_REGISTRY (or the serializer’s allowed_classes allowlist) — never through dynamic imports.
Decorator Syntax
Section titled “Decorator Syntax”Decorate async functions with @cache or @cacheable:
from oridecon.cache import cache, cacheable
@cache(ttl=300, tags=["user"])async def get_user(user_id: str) -> dict: return await db.fetch_user(user_id)
@cacheable(ttl=60)async def expensive_computation(input: str) -> str: # result is cached automatically return await compute(input)Typical Usage
Section titled “Typical Usage”1. Wire the cache
Section titled “1. Wire the cache”from oridecon import Applicationfrom oridecon.cache import CacheModulefrom oridecon.cache.config import CacheConfig
def create_app() -> Application: app = Application(name="my-app") app.add_module(CacheModule.configure(CacheConfig( backends=[{ "name": "default", "type": "memory", "default": True, }], ))) return app2. Use in a service
Section titled “2. Use in a service”from oridecon.di import injectfrom oridecon.cache import CacheService
class UserService: @inject def __init__(self, cache: CacheService) -> None: self.cache = cache
async def get_user(self, user_id: str) -> dict: cached = await self.cache.get(f"user:{user_id}") if cached is not None: return cached user = await self._load_from_db(user_id) await self.cache.set(f"user:{user_id}", user, ttl=300) return userBest Practices
Section titled “Best Practices”- ✅ Use
MemoryCacheBackendfor testing — no external dependencies - ✅ Set
default_ttlon backends to prevent unbounded cache growth - ✅ Use tag-based invalidation for group cache clearing
- ✅ Enable stampede protection for expensive-to-compute values
- ⚠️ Cache only JSON-serializable values; for non-serializable objects, register the value type in the deny-by-default type registry (
DEFAULT_REGISTRY) or use a customAsyncStringSerializerProtocol - ❌ Don’t cache user secrets (passwords, tokens) in plain text
- ❌ Don’t skip TTL for volatile data — always set an expiration
Next Steps
Section titled “Next Steps”- How-Tos — Redis setup, stampede protection, multi-backend
- Configuration — all configuration keys
- Troubleshooting — common errors and fixes