base-cacheable-class: BaseCacheableClass: Caching as a Dependency, Not a Decorator
An async Python framework that routes the same service through memory, Redis, or mocks at runtime, then invalidates only the keys that match.
- BaseCacheableClass turns caching into a runtime dependency, so the same service code can point at memory, Redis, or a mock without changing the method body.
- Its real innovation is control, because the abstraction decides where cache behavior lives and when it can be swapped.
- Structured keys and pattern matching let invalidation remove one branch of state instead of flushing the whole store.
- The repo is small, but its interfaces, typing, and async-first shape make it feel production-minded rather than decorative.
Most cache helpers answer a narrow question: how do I store the result? BaseCacheableClass asks a better one: who gets to decide the cache at runtime? That shift is the whole article. The library does not just decorate methods. It makes caching feel like dependency injection for service classes.
The decorator that waits until runtime
The core trick is that cache and invalidate are not hardwired to one backend. They resolve their real behavior from the instance, via self._cache_decorator, when the method is called. That means the class body stays stable while the cache strategy changes underneath it. In production, you can route through Redis. In tests, you can swap in a mock. In local development, you can stay in memory.
class UserService(BaseCacheableClass):
def __init__(self, cache_decorator):
super().__init__(cache_decorator)
@BaseCacheableClass.cache()
async def get_user(self, user_id: int) -> dict:
...
Why invalidation is the real invention
A lot of caching libraries make reads easy. This one spends its ingenuity on the harder part: removing only the entries that are now wrong. The repo builds structured keys from the target function and its arguments, then uses pattern matching to prune the affected branch instead of blasting the whole store. That is the difference between a blunt flush and a surgical cleanup.
The interfaces are the product
The repo’s discipline shows up in its contracts. CacheInterface defines the storage behavior. CacheDecoratorInterface defines how methods get wrapped. That separation lets the library do something practical that many cache utilities never quite nail: backend swapping without rewriting the service layer. It also makes a multi-tier setup plausible, where memory can sit in front of Redis and a test double can stand in for both.
That matters because the abstraction is not trying to be clever for its own sake. It is trying to make cache behavior injectable, inspectable, and replaceable. When the interface is right, the call site stays clean and the tests stop fighting the infrastructure.
Built for async service code
This is a modern Python utility, not a generic memoizer. The codebase is built around Python 3.10 style typing, async methods, pytest-asyncio, strict linting, and a package workflow centered on uv. The in-memory backend keeps the surface area small. The Redis backend adds distributed reach, with serialization and non-blocking key scans so invalidation does not depend on blunt key sweeps.
The result is a library that feels designed for service objects, not toy functions. It wants to sit inside an API layer, a worker, or a domain service where the cache is one concern among several, not the whole story.
What it replaces, and what it does not
| Approach | Runtime backend swap | Granular invalidation | Async-first fit | Testability | Best use case |
|---|---|---|---|---|---|
| functools.lru_cache | No | No | Limited | Medium | Pure function memoization |
| Direct Redis key management | Yes, but manual | Yes, if you build it | Strong | Low to medium | Infrastructure-level control |
| BaseCacheableClass | Yes | Yes | Strong | High | Async service methods with injectable cache backends |
Small repo, serious habits
The repository still reads as early-stage software, not a finished platform. That is visible in the narrow scope and the roadmap signal around synchronous support. But the design choices are not casual. The interfaces are clean. The examples are practical. The invalidation story is specific. In a space full of cache wrappers that stop at the happy path, this one shows real attention to the failures that matter.