Core - Caching Infrastructure
Location: api/nextango/apps/core/utils/cache.py
Last Updated: 2026-07-09
Overview
The caching infrastructure provides Redis-backed response caching for DRF ViewSets with automatic invalidation on model changes. Built on drf-extensions, it integrates with Django signals to ensure cache consistency.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ ViewSet Endpoint │
│ (list, retrieve actions) │
└──────────────────────┬──────────────────────────────────────┘
│ @cache_response decorator
┌──────────────────────▼──────────────────────────────────────┐
│ drf-extensions cache_response │
│ │
│ Key Function: make_cache_key_func('prefix') │
│ Key Pattern: {prefix}:{path}:{query_params} │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────────┐
│ Redis Cache │
│ │
│ brand:/api/brands/: │
│ brand:/api/brands/:slug=nike │
│ category:/api/categories/:parent=electronics │
└──────────────────────┬──────────────────────────────────────┘
│ On model save/delete
┌──────────────────────▼──────────────────────────────────────┐
│ Django Signal Handlers │
│ │
│ post_save → invalidate_cache_pattern('brand:') │
│ post_delete → invalidate_cache_pattern('brand:') │
└─────────────────────────────────────────────────────────────┘
Cache Timeouts by Domain
| Domain | Prefix | Timeout | Rationale |
|---|---|---|---|
| Brands | brand: | 3600s (1 hour) | Rarely changes |
| Categories | category: | 3600s (1 hour) | Rarely changes |
| Collections | collection: | 3600s (1 hour) | Rarely changes |
| Product Types | producttype: | 3600s (1 hour) | Rarely changes |
| Promo Codes | promocode: | 3600s (1 hour) | Moderate change frequency |
| Campaigns | campaign: | 1800s (30 min) | More frequent updates |
| Zones | zone: | 1200s (20 min) | Moderate change frequency |
| Stores | store: | 600s (10 min) | Settings change frequently |
API Reference
make_cache_key_func(prefix)
Source: cache.py:141-174
Generate a cache key function for drf-extensions @cache_response decorator.
Signature:
def make_cache_key_func(prefix: str) -> Callable
Parameters:
| Parameter | Type | Description |
|---|---|---|
prefix | str | Cache key prefix (e.g., 'brand', 'category') |
Returns: Key function compatible with @cache_response
Key Pattern: {prefix}:{path}:{query_params_sorted}
Example:
from rest_framework_extensions.cache.decorators import cache_response
from nextango.apps.core.utils.cache import make_cache_key_func
class BrandViewSet(viewsets.ModelViewSet):
queryset = Brand.objects.all()
serializer_class = BrandSerializer
@cache_response(timeout=3600, key_func=make_cache_key_func('brand'))
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@cache_response(timeout=3600, key_func=make_cache_key_func('brand'))
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
Generated Keys:
brand:/api/brands/ # List all brands
brand:/api/brands/:slug=nike # Filter by slug
brand:/api/brands/nike # Retrieve single brand
invalidate_cache_pattern(pattern)
Source: cache.py:100-138
Invalidate all cache keys matching a prefix pattern.
Signature:
def invalidate_cache_pattern(pattern: str) -> None
Parameters:
| Parameter | Type | Description |
|---|---|---|
pattern | str | Prefix pattern to match (e.g., 'brand:') |
Behavior:
- Connects to Redis using
django-redis - Iterates keys matching
{KEY_PREFIX}:1:{pattern}*with SCAN (non-blocking, batches of 100) rather than KEYS - Deletes each matched batch
- Logs the number of invalidated keys
Example:
from nextango.apps.core.utils.cache import invalidate_cache_pattern
# In signals.py
@receiver(post_save, sender=Brand)
def invalidate_brand_cache(sender, instance, **kwargs):
invalidate_cache_pattern('brand:')
logger.debug(f"Invalidated brand cache after {instance.slug} save")
Log Output:
INFO Invalidated 15 cache keys matching prefix: brand:
cache_result(timeout, key_prefix, vary_on)
Source: cache.py:11-97
Decorator to cache function results in Redis (for non-view functions).
Signature:
@cache_result(timeout=300, key_prefix=None, vary_on=None)
def my_function(...):
...
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
timeout | int | 300 | Cache timeout in seconds |
key_prefix | str | None | Optional prefix for cache key |
vary_on | list | None | Parameter names to include in cache key |
Example:
from nextango.apps.core.utils.cache import cache_result
@cache_result(timeout=600, key_prefix='products', vary_on=['store_id'])
def get_products_for_display(store_id=None, category_id=None):
"""Get products for store display - cached per store."""
return Product.objects.filter(
store_id=store_id,
is_active=True
).values('id', 'name', 'price')
Key Generation:
- With
vary_on: Only specified parameters included in key - Without
vary_on: All args and kwargs included - Keys > 200 chars are hashed with MD5
clear_cache()
Source: cache.py:177-183
Clear all cache entries (use with caution).
from nextango.apps.core.utils.cache import clear_cache
# Clear everything - typically for maintenance/debugging
clear_cache()
cache_key(*parts)
Source: cache.py:186-188
Generate a cache key from parts.
from nextango.apps.core.utils.cache import cache_key
key = cache_key('user', user_id, 'preferences')
# Returns: "user:123:preferences"
Implementation Patterns
ViewSet Caching Pattern
Standard pattern for caching ViewSet list and retrieve actions:
# views.py
from rest_framework import viewsets
from rest_framework_extensions.cache.decorators import cache_response
from nextango.apps.core.utils.cache import make_cache_key_func
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
lookup_field = 'slug'
@cache_response(timeout=3600, key_func=make_cache_key_func('category'))
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@cache_response(timeout=3600, key_func=make_cache_key_func('category'))
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
Signal Invalidation Pattern
Invalidate cache when models change:
# signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from nextango.apps.core.utils.cache import invalidate_cache_pattern
from .models import Category
@receiver(post_save, sender=Category)
@receiver(post_delete, sender=Category)
def invalidate_category_cache(sender, instance, **kwargs):
"""Invalidate category cache on any change."""
invalidate_cache_pattern('category:')
Bulk Import Cache Invalidation
After bulk imports, invalidate all affected caches:
# management/commands/import_from_sanity.py
def handle(self, *args, **options):
# ... import logic ...
# Invalidate all reference data caches
from nextango.apps.core.utils.cache import invalidate_cache_pattern
prefixes = ['brand:', 'category:', 'producttype:', 'collection:', 'zone:', 'store:']
for prefix in prefixes:
invalidate_cache_pattern(prefix)
self.stdout.write("Cache invalidated for all reference data")
Domains Using Caching
Products Domain
File: api/nextango/apps/products/views.py
| ViewSet | Prefix | Timeout | Actions Cached |
|---|---|---|---|
BrandViewSet | brand: | 3600s | list, retrieve |
ProductTypeViewSet | producttype: | 3600s | list, retrieve |
CategoryViewSet | category: | 3600s | list, retrieve |
CollectionViewSet | collection: | 3600s | list, retrieve |
Invalidation Signals: api/nextango/apps/products/signals.py:561-585
Promotions Domain
File: api/nextango/apps/promotions/views.py
| ViewSet | Prefix | Timeout | Actions Cached |
|---|---|---|---|
PromotionalCampaignViewSet | campaign: | 1800s | list, retrieve |
PromoCodeViewSet | promocode: | 3600s | list, retrieve |
Invalidation Signals: api/nextango/apps/promotions/signals.py:361-369
Stores Domain
File: api/nextango/apps/stores/views.py
| ViewSet | Prefix | Timeout | Actions Cached |
|---|---|---|---|
StoreViewSet | store: | 600s | list |
Invalidation Signals: api/nextango/apps/stores/signals.py:214-219
Zones Domain
File: api/nextango/apps/zones/views.py
| ViewSet | Prefix | Timeout | Actions Cached |
|---|---|---|---|
ZoneViewSet | zone: | 1200s | list, retrieve |
Invalidation Signals: api/nextango/apps/zones/signals.py:116
Performance Considerations
Cache Hit Ratio
Monitor cache effectiveness with Redis CLI:
redis-cli INFO stats | grep keyspace
# keyspace_hits:12345
# keyspace_misses:123
# Hit ratio = hits / (hits + misses)
Memory Usage
Check cache memory:
redis-cli INFO memory | grep used_memory_human
# used_memory_human:256.50M
Key Count by Prefix
redis-cli KEYS "nextango:1:brand:*" | wc -l
redis-cli KEYS "nextango:1:category:*" | wc -l
Troubleshooting
Cache Not Invalidating
- Check signal is connected:
from django.db.models.signals import post_save
print(post_save.receivers) # Should include invalidation handler
- Verify Redis connection:
from django_redis import get_redis_connection
conn = get_redis_connection("default")
conn.ping() # Should return True
- Check KEY_PREFIX setting:
from django.conf import settings
print(settings.CACHES['default'].get('KEY_PREFIX'))
Stale Data After Update
If cache returns old data after model update:
- Check if signal handler raised an exception
- Verify the correct prefix is being invalidated
- Manually invalidate:
from nextango.apps.core.utils.cache import invalidate_cache_pattern
invalidate_cache_pattern('brand:')
Related Documentation
- Products Views - Product domain caching
- Promotions Views - Promotions domain caching
- Stores Views - Stores domain caching
- Zones Views - Zones domain caching