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

DomainPrefixTimeoutRationale
Brandsbrand:3600s (1 hour)Rarely changes
Categoriescategory:3600s (1 hour)Rarely changes
Collectionscollection:3600s (1 hour)Rarely changes
Product Typesproducttype:3600s (1 hour)Rarely changes
Promo Codespromocode:3600s (1 hour)Moderate change frequency
Campaignscampaign:1800s (30 min)More frequent updates
Zoneszone:1200s (20 min)Moderate change frequency
Storesstore: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:

ParameterTypeDescription
prefixstrCache 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:

ParameterTypeDescription
patternstrPrefix pattern to match (e.g., 'brand:')

Behavior:

  1. Connects to Redis using django-redis
  2. Iterates keys matching {KEY_PREFIX}:1:{pattern}* with SCAN (non-blocking, batches of 100) rather than KEYS
  3. Deletes each matched batch
  4. 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:

ParameterTypeDefaultDescription
timeoutint300Cache timeout in seconds
key_prefixstrNoneOptional prefix for cache key
vary_onlistNoneParameter 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

ViewSetPrefixTimeoutActions Cached
BrandViewSetbrand:3600slist, retrieve
ProductTypeViewSetproducttype:3600slist, retrieve
CategoryViewSetcategory:3600slist, retrieve
CollectionViewSetcollection:3600slist, retrieve

Invalidation Signals: api/nextango/apps/products/signals.py:561-585

Promotions Domain

File: api/nextango/apps/promotions/views.py

ViewSetPrefixTimeoutActions Cached
PromotionalCampaignViewSetcampaign:1800slist, retrieve
PromoCodeViewSetpromocode:3600slist, retrieve

Invalidation Signals: api/nextango/apps/promotions/signals.py:361-369

Stores Domain

File: api/nextango/apps/stores/views.py

ViewSetPrefixTimeoutActions Cached
StoreViewSetstore:600slist

Invalidation Signals: api/nextango/apps/stores/signals.py:214-219

Zones Domain

File: api/nextango/apps/zones/views.py

ViewSetPrefixTimeoutActions Cached
ZoneViewSetzone:1200slist, 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

  1. Check signal is connected:
from django.db.models.signals import post_save
print(post_save.receivers)  # Should include invalidation handler
  1. Verify Redis connection:
from django_redis import get_redis_connection
conn = get_redis_connection("default")
conn.ping()  # Should return True
  1. 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:

  1. Check if signal handler raised an exception
  2. Verify the correct prefix is being invalidated
  3. Manually invalidate:
from nextango.apps.core.utils.cache import invalidate_cache_pattern
invalidate_cache_pattern('brand:')

Was this page helpful?