Skip to content
Packages Examples Agents Blog Get started
PackageRequiredPurpose
orideconYesCore framework
oridecon-contractsYesProtocol definitions
redisRecommendedRedis cache backend
pymemcacheOptionalMemcached cache backend

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.

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)

All backends implement CacheBackendProtocol from oridecon.contracts.infra.cache. The protocol returns Result[T, CacheError] for every operation:

from oridecon.contracts.infra.cache import CacheBackendProtocol
from oridecon.result import Ok, Err
# Backend returns Result — CacheService unwraps it internally
result = await backend.get("my-key")
if result.is_ok():
value = result.unwrap()
BackendExtraStorage
MemoryCacheBackendNoneIn-process dict
RedisCacheBackendoridecon-cache[redis]Redis server
MemcachedCacheBackendoridecon-cache[memcached]Memcached server

CacheService wraps the backend and provides ergonomic access:

from oridecon.cache import CacheService
# Resolved from the container — inject via constructor
await cache.set("user:42", {"name": "Alice"}, ttl=300)
value = await cache.get("user:42") # → {"name": "Alice"}
await cache.delete("user:42") # → True
count = await cache.delete_pattern("user:*") # → number of keys

CacheService.get() returns the raw value or default (not Result). Errors are logged and return the default — the service prefers graceful degradation over crashing.

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: 6379

Resolve a specific backend:

hot_cache = await container.resolve(CacheService, name="hot")
persistent_cache = await container.resolve(CacheService, name="persistent")

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.0

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 articles
await cache.invalidate_tags(["articles"])
# The next get() for article:1 returns None

CacheService serializes values to JSON by default. JSON is the only built-in safe serializer — pickle is not available:

cache.service:
default_serializer: "json" # default

Available 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.

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)
from oridecon import Application
from oridecon.cache import CacheModule
from 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 app
from oridecon.di import inject
from 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 user
  • Use MemoryCacheBackend for testing — no external dependencies
  • Set default_ttl on 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 custom AsyncStringSerializerProtocol
  • Don’t cache user secrets (passwords, tokens) in plain text
  • Don’t skip TTL for volatile data — always set an expiration