Backends

Nicolas Cache supports multiple backend implementations, each with its own characteristics and use cases.

Memory Backend

The memory backend stores cache data in Python dictionaries within the application’s memory space.

Characteristics:

  • Speed: Fastest possible access times

  • Persistence: No persistence - data is lost on application restart

  • Scalability: Limited to available RAM

  • Distribution: Not shared between processes

  • TTL Support: No automatic expiration

Configuration:

from nicolas.cache import Cache

cache = Cache(backend="memory")

Use Cases:

  • Development and testing

  • Small datasets that fit in memory

  • Temporary data that doesn’t need persistence

  • Single-process applications

Implementation Details:

The memory backend uses:

  • dict for storing key-value pairs

  • dict for tag registry

  • set for tracking keys per tag

Redis Backend

The Redis backend uses Redis server for distributed caching with optional persistence.

Characteristics:

  • Speed: Very fast, with network overhead

  • Persistence: Optional (RDB snapshots, AOF logs)

  • Scalability: Can handle large datasets

  • Distribution: Shared between multiple processes/servers

  • TTL Support: Native TTL support

Configuration:

from nicolas.cache import Cache

cache = Cache(
    backend="redis",
    host="localhost",      # Redis server hostname
    port=6379,            # Redis server port
    db=0,                 # Database number (0-15)
    password=None,        # Authentication password
    prefix="cache:"       # Key prefix for namespacing
)

Advanced Configuration:

# With authentication
cache = Cache(
    backend="redis",
    host="redis.example.com",
    port=6379,
    password="secret_password"
)

# With custom prefix for multi-tenancy
cache = Cache(
    backend="redis",
    prefix=f"tenant_{tenant_id}:cache:"
)

TTL (Time-To-Live) Support:

# Set with expiration
cache.set("session", data, ttl=3600)  # Expires in 1 hour

# Key will automatically be removed after TTL
import time
time.sleep(3601)
assert cache.get("session") is None

Use Cases:

  • Production environments

  • Distributed applications

  • Session storage

  • Rate limiting

  • Temporary data with expiration

Redis Data Structure:

The Redis backend uses:

  • Strings for cache values (pickled)

  • Sets for tag registry

  • Sets for tracking keys per tag

Example Redis structure:

cache:user:1                    -> pickled user object
cache:tag:users                 -> {user:1, user:2, ...}
cache:key_tags:user:1           -> {users, active, ...}

Redis Sentinel Backend

Redis Sentinel provides high availability with automatic failover.

Characteristics:

  • High Availability: Automatic failover on master failure

  • Read/Write Splitting: Reads from slaves, writes to master

  • Monitoring: Built-in health checking

  • Notification: Alerts on topology changes

  • All Redis features: TTL, persistence, etc.

Configuration:

from nicolas.cache import Cache

cache = Cache(
    backend="redis-sentinel",
    sentinels=[                    # List of sentinel nodes
        ("sentinel1.example.com", 26379),
        ("sentinel2.example.com", 26379),
        ("sentinel3.example.com", 26379)
    ],
    service_name="mymaster",       # Redis service name in Sentinel
    db=0,                         # Database number
    password="redis_password",     # Redis auth password
    sentinel_password="sent_pass", # Sentinel auth password
    socket_timeout=0.1,           # Connection timeout
    socket_keepalive=True         # TCP keepalive
)

Sentinel Setup Example:

  1. sentinel.conf configuration:

port 26379
sentinel monitor mymaster 192.168.1.10 6379 2
sentinel auth-pass mymaster redis_password
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 10000
  1. Start Sentinel:

$ redis-sentinel /path/to/sentinel.conf

Automatic Failover:

The Sentinel backend automatically handles failover:

# Normal operation - writes go to master
cache.set("key", "value")

# If master fails, Sentinel promotes a slave
# The cache client automatically reconnects to new master
cache.set("key", "new_value")  # Works seamlessly

Use Cases:

  • Mission-critical applications

  • Zero-downtime requirements

  • Multi-datacenter deployments

  • Production environments requiring HA

Backend Comparison

Backend Feature Comparison

Feature

Memory

Redis

Redis Sentinel

Notes

Speed

★★★★★

★★★★☆

★★★★☆

Memory is fastest

Persistence

Redis can persist to disk

Distributed

Shared between processes

TTL Support

Automatic expiration

High Availability

Automatic failover

Memory Usage

High

Low

Low

Memory backend uses app RAM

Setup Complexity

None

Low

Medium

Sentinel requires configuration

Choosing a Backend

Use Memory Backend when:

  • Developing and testing

  • Data fits in memory

  • Single process/thread

  • Speed is critical

  • No persistence needed

Use Redis Backend when:

  • Need persistence

  • Multiple processes/servers

  • TTL support required

  • Large datasets

  • Production environment

Use Redis Sentinel when:

  • High availability required

  • Zero downtime critical

  • Automatic failover needed

  • Multi-datacenter setup

  • Mission-critical data

Custom Backend Implementation

You can create custom backends by implementing the CacheBackend interface:

from nicolas import CacheBackend
from typing import Any, Dict, Optional, Iterable

class CustomBackend(CacheBackend):
    """Custom cache backend implementation."""

    def get(self, cache_key: str) -> Any:
        """Retrieve a value from the cache."""
        pass

    def get_by_tag(self, tag: str) -> Dict[str, Any]:
        """Get all entries with a specific tag."""
        pass

    def getall(self) -> Dict[str, Any]:
        """Get all cache entries."""
        pass

    def set(self, cache_key: str, value: Any,
            tags: Optional[Iterable[str]] = None) -> None:
        """Store a value in the cache."""
        pass

    def delete(self, cache_key: str) -> bool:
        """Delete a cache entry."""
        pass

    def delete_by_tag(self, tag: str) -> int:
        """Delete all entries with a tag."""
        pass

    def exists(self, cache_key: str) -> bool:
        """Check if a key exists."""
        pass

Then register and use your backend:

from nicolas.cache import Cache

# Extend Cache class to support custom backend
class ExtendedCache(Cache):
    def __init__(self, backend: str = "memory", **kwargs):
        if backend == "custom":
            self._backend = CustomBackend(**kwargs)
        else:
            super().__init__(backend, **kwargs)

# Use custom backend
cache = ExtendedCache(backend="custom")