Surgical Cache Invalidation
You have an API that caches user profiles. When a user updates their profile, you need to evict exactly their entry - not every entry, not even every user entry. Just that one.
Before v1.0, you had two options: cache_clear() (nukes everything for that function) or manual store.delete() (requires knowing the exact key format). Neither is great.
cache_invalidate() builds the same key your function would use and deletes it. One call, one entry gone.
The problem: a user profile API
from inhouse import inhouse_cache, MemoryStore
store = MemoryStore(max_size=1024, default_ttl=300)
@inhouse_cache(store=store)
def load_profile(db: object, user_id: int) -> dict:
row = db.execute(
"SELECT name, email, avatar FROM users WHERE id = ?", (user_id,)
).fetchone()
return dict(row) if row else None
The db argument is a database connection. Every connection object is different, so the same user_id with a different db handle produces a different cache key. That means cache misses even for identical queries.
Fix: exclude the database connection
With exclude=, you drop db from the key. Now load_profile(conn_a, 42) and load_profile(conn_b, 42) share one entry.
@inhouse_cache(store=store, exclude=("db",))
def load_profile(db: object, user_id: int) -> dict:
row = db.execute(
"SELECT name, email, avatar FROM users WHERE id = ?", (user_id,)
).fetchone()
return dict(row) if row else None
No custom key_builder. No manual key construction. One parameter name and it is done.
Evict one entry after a write
The user updates their avatar. The profile page should show the new image on the next load.
def update_avatar(db: object, user_id: int, new_avatar: str):
db.execute(
"UPDATE users SET avatar = ? WHERE id = ?", (new_avatar, user_id)
)
db.commit()
# evict exactly this user's cached profile
load_profile.cache_invalidate(db, user_id=user_id)
The key is built with the same exclude=("db",) rule, so the db argument is ignored. The user_id=42 part determines which key to delete. The next call to load_profile(any_db, 42) is a cache miss and refreshes from the database.
Multiple parameters, surgical eviction
A dashboard with filters needs to evict one combination of parameters when data changes.
@inhouse_cache(store=store, exclude=("db",))
def load_usage_report(db: object, team_id: int, period: str, region: str | None = None) -> dict:
...
# New data ingested for team 5, Q3, US region:
load_usage_report.cache_invalidate(db, team_id=5, period="Q3", region="US")
Only the entry for team_id=5, period="Q3", region="US" is evicted. Other teams, other periods, other regions are untouched.
Check if the eviction actually happened
cache_invalidate() returns True if a key was removed, False if it did not exist.
if load_profile.cache_invalidate(db, user_id=999):
print("Evicted user 999's cached profile")
else:
print("User 999 was not in cache (maybe TTL expired)")
Useful for logging or conditional logic in write paths.
Multiple stores, same pattern
If you have hot and cold stores, cache_invalidate works on whichever store the decorator was configured with.
hot_store = MemoryStore(default_ttl=30)
cold_store = MemoryStore(default_ttl=3600)
@inhouse_cache(store=hot_store, exclude=("db",))
def load_recent_activity(db: object, team_id: int) -> list[dict]:
...
@inhouse_cache(store=cold_store, exclude=("db",))
def load_team_settings(db: object, team_id: int) -> dict:
...
# After a new event is ingested:
load_recent_activity.cache_invalidate(db, team_id=5)
# Team settings stay cached - different function, different store
When to use each invalidation method
| Method | Scope | Use case |
|---|---|---|
cache_invalidate(*args, **kwargs) | One call signature | After a write that affects a specific entity |
cache_clear() | All entries for a function | After a bulk operation or schema change |
store.delete(prefix) | Custom key prefix | When you know the exact key format |
store.clear() | Entire store | Emergency flush |
The recipe in production
from inhouse import MemoryStore, inhouse_cache
store = MemoryStore(max_size=2048, default_ttl=60)
@inhouse_cache(store=store, exclude=("db",))
def get_user_profile(db, user_id: int) -> dict | None:
...
@inhouse_cache(store=store, exclude=("db",))
def get_user_orders(db, user_id: int, limit: int = 10) -> list[dict]:
...
def place_order(db, user_id: int, order_data: dict) -> int:
order_id = insert_order(db, order_data)
db.commit()
# Evict this user's cached data
get_user_profile.cache_invalidate(db, user_id=user_id)
get_user_orders.cache_invalidate(db, user_id=user_id)
return order_id
The profile and orders cache entries for user_id=42 are evicted after the write. Every other user's cache is untouched.