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.

13 min read • View on GitHub • More from instructkr

A hand-drawn service class shown as a switchboard in the center, with three cache cartridges branching off to memory, Redis, and a test double. A single method call enters the class, and the routing decision happens inside the class instead of outside it. The image explains that caching here behaves like an injectable runtime dependency rather than a fixed decorator.
The same service can point at different cache backends without rewriting its methods.
Key Takeaways

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

The decorator does not own the backend. It asks the instance which backend to use, then follows that answer.

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.

A close-up of a key rack or filing drawer where one labeled cache key is being pulled out with tweezers while nearby keys remain untouched. The composition emphasizes precision and restraint. The image explains that invalidation is selective, not a full reset.
Invalidation here behaves like a scalpel, not a broom.

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

ApproachRuntime backend swapGranular invalidationAsync-first fitTestabilityBest use case
functools.lru_cacheNoNoLimitedMediumPure function memoization
Direct Redis key managementYes, but manualYes, if you build itStrongLow to mediumInfrastructure-level control
BaseCacheableClassYesYesStrongHighAsync 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.