The Cache Framework Architecture
The cache framework provides a unified interface for interacting with various storage backends, such as Memcached, Redis, or database-backed caches. This architecture is built around a central handler that manages backend connections and a base interface that defines the standard caching operations.
Connection Management and the CacheHandler
The CacheHandler class (found in django/core/cache/__init__.py) is the primary entry point for accessing cache backends. It inherits from BaseConnectionHandler and is responsible for instantiating cache backend objects based on the CACHES setting in the project configuration.
When a developer accesses a cache via caches['alias'], the CacheHandler performs the following steps in its create_connection method:
# django/core/cache/__init__.py
class CacheHandler(BaseConnectionHandler):
settings_name = "CACHES"
exception_class = InvalidCacheBackendError
def create_connection(self, alias):
params = self.settings[alias].copy()
backend = params.pop("BACKEND")
location = params.pop("LOCATION", "")
try:
backend_cls = import_string(backend)
except ImportError as e:
raise InvalidCacheBackendError(
"Could not find backend '%s': %s" % (backend, e)
) from e
return backend_cls(location, params)
The handler uses import_string to dynamically load the backend class specified in the BACKEND setting. If the backend cannot be found, it raises an InvalidCacheBackendError.
The Cache Proxy
For convenience, the framework provides a cache object, which is a ConnectionProxy. This proxy always points to the default cache alias, allowing for simple access without explicitly referencing the caches handler:
# django/core/cache/__init__.py
cache = ConnectionProxy(caches, DEFAULT_CACHE_ALIAS)
The BaseCache Interface
All cache backends must inherit from BaseCache (django/core/cache/backends/base.py). This abstract base class defines the standard API that ensures consistency across different storage implementations.
Core Operations
BaseCache defines several fundamental methods that subclasses are required to implement:
add(key, value, timeout, version): Sets a value only if the key does not exist.get(key, default, version): Retrieves a value.set(key, value, timeout, version): Sets a value, overwriting any existing data.delete(key, version): Removes a key.clear(): Flushes the entire cache.
Async Support
The framework includes asynchronous versions of all core methods (e.g., aget, aset, adelete). By default, these methods use sync_to_async to wrap their synchronous counterparts, but backends can override them for native async support:
# django/core/cache/backends/base.py
async def aget(self, key, default=None, version=None):
return await sync_to_async(self.get, thread_sensitive=True)(
key, default, version
)
Default Implementations
BaseCache provides logic for complex operations that build upon the core methods. For example, get_or_set attempts to retrieve a key and, if missing, sets it to a default value (which can be a callable):
# django/core/cache/backends/base.py
def get_or_set(self, key, default, timeout=DEFAULT_TIMEOUT, version=None):
val = self.get(key, self._missing_key, version=version)
if val is self._missing_key:
if callable(default):
default = default()
self.add(key, default, timeout=timeout, version=version)
return self.get(key, default, version=version)
return val
Key Management and Validation
The framework handles key transformation and validation centrally in BaseCache to ensure portability across backends.
Key Construction
The make_key method combines the raw key with a KEY_PREFIX and a version number. This allows for namespacing and easy cache invalidation by incrementing versions.
def make_key(self, key, version=None):
if version is None:
version = self.version
return self.key_func(key, self.key_prefix, version)
Validation and Portability
Different backends have different restrictions on keys (e.g., Memcached limits keys to 250 characters and forbids control characters). BaseCache.validate_key issues a CacheKeyWarning if a key might cause issues on other backends, encouraging developers to write portable code.
If a backend strictly cannot handle a key, it may raise an InvalidCacheKey exception, which is a subclass of ValueError.
Lifecycle and Cleanup
Cache connections often require explicit cleanup, such as closing network sockets. The framework automates this by connecting the close_caches function to the request_finished signal.
# django/core/cache/__init__.py
def close_caches(**kwargs):
caches.close_all()
signals.request_finished.connect(close_caches)
This ensures that every cache backend instantiated during a request-response cycle has its close() method called once the request is complete, preventing connection leaks. Backends implement this by overriding the close() method in their BaseCache subclass.