Semantic Invalidation That Doesn't Suck
Source: Dev.to
The caching problem
If you’ve worked on a web app for any length of time, you know the deal with caching. You add a cache, everything’s fast, and then someone updates something and users see old data—prices, inventory, whatever. TTL helps, but you’re always trading freshness for load.
The typical fix is manual invalidation: update a product, invalidate the cache key. This works for a single endpoint, but quickly becomes messy when that product has reviews, reviews have comments, the product belongs to a store, and the store belongs to an organization. You end up tracking many relationships and invalidating keys everywhere.
Introducing ZooCache
ZooCache is a Python caching library with a Rust core that focuses on semantic invalidation—invalidating based on what changed, not just when.
Registering dependencies
When you cache something, you declare the tags (dependencies) that represent the data it depends on:
from zoocache import cacheable, invalidate, add_deps, configure
configure()
@cacheable()
def get_product(pid):
add_deps([f"product:{pid}"])
return db.get_product(pid)
@cacheable()
def get_reviews(pid):
add_deps([f"product:{pid}:reviews"])
return db.get_reviews(pid)
@cacheable()
def get_store_products(sid):
add_deps([f"store:{sid}:products"])
return db.get_store_products(sid)
@cacheable()
def get_org_stores(oid):
add_deps([f"org:{oid}:stores"])
return db.get_org_stores(oid)
These tags form a hierarchy. For example, org:1:stores:2:products:42 is a path in a PrefixTrie.
Invalidating tags
When data changes, you invalidate the relevant tag:
def update_product(pid, data):
db.update_product(pid, data)
invalidate(f"product:{pid}")
def update_store(sid, data):
db.update_store(sid, data)
invalidate(f"store:{sid}")
def update_org(oid, data):
db.update_org(oid, data)
invalidate(f"org:{oid}")
Invalidating org:1 clears everything below it—products, reviews, store products, etc. You no longer need to remember which functions cached what.
The invalidation operation is O(D) where D is the tag depth, independent of the number of cached items.
Consistency across instances
If you run multiple instances, ZooCache uses Hybrid Logical Clocks (HLC) for consistency. Each invalidation receives a timestamp that accounts for clock drift, guaranteeing that later invalidations have higher timestamps even when system clocks are unsynchronized.
Passive resynchronization
Every cached entry stores version information. When a node reads data from another node, it checks those versions; if they are newer, the node automatically catches up. This keeps reads consistent without extra coordination.
Preventing cache stampedes
When a cache entry expires and many requests hit the database simultaneously, you get a stampede. ZooCache mitigates this with a SingleFlight pattern: the first request performs the work while the others wait, then all receive the same result. The database sees only one query instead of many.
Features at a glance
- Storage backends: in‑memory, LMDB, or Redis
- Framework integrations: FastAPI, Django, Litestar
- Serialization: MsgPack with LZ4 compression
- Observability: logs, Prometheus, OpenTelemetry
- CLI for monitoring
- Rust core with Python bindings
Benchmarks are available on the documentation site.
Installation
uv add zoocache
Links
Feel free to try it out and share your thoughts—issues, suggestions, or anything else. We’re always happy to hear how things could work better.