Semantic Invalidation That Doesn't Suck

Published: (March 3, 2026 at 09:45 AM EST)
3 min read
Source: Dev.to

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

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.

0 views
Back to Blog

Related posts

Read more »