Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Caching

Djangors provides a unified key-value caching system. It supports multiple storage backends, flexible Time-To-Live (TTL) configuration, JSON serialization helpers, and opt-in HTTP response caching middleware.


The Cache Trait

At the core of the caching system is the Cache trait from djangors_cache. It defines the essential operations for a raw-byte key-value store:

#![allow(unused)]
fn main() {
use async_trait::async_trait;
use std::time::Duration;
use djangors::cache::CacheError;

#[async_trait]
pub trait Cache: Send + Sync {
    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, CacheError>;
    async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<(), CacheError>;
    async fn delete(&self, key: &str) -> Result<(), CacheError>;
}
}

High-level Helpers via CacheExt

To simplify working with cached structures, Djangors provides CacheExt which automatically extends any implementation of Cache with:

  • get_or_set: Looks up a key; if missing, executes a closure to compute the raw bytes, caches them, and returns the result.
  • get_or_set_json: Automatically handles JSON serialization and deserialization using serde_json.
#![allow(unused)]
fn main() {
use djangors::cache::CacheExt;
use std::time::Duration;

let value = cache.get_or_set_json("my_key", Some(Duration::from_secs(60)), || async {
    // Heavy computation returning Result<MyStruct, CacheError>
    Ok(MyStruct { data: 42 })
}).await?;
}

The free function get_or_set_fragment

get_or_set_fragment is the same compute-if-missing pattern as a standalone function for call sites that do not have a concrete Cache impl, only the dyn Cache trait object (for example, an async handler receiving the store as Arc<dyn Cache>):

use std::sync::Arc;
use djangors_cache::{Cache, CacheError, InMemoryCache, get_or_set_fragment};
use std::time::Duration;
fn main() {}
async fn run() -> Result<(), CacheError> {
let store: Arc<dyn Cache> = Arc::new(InMemoryCache::new(10_000));
let html = get_or_set_fragment(&*store, "template:home", Some(Duration::from_secs(300)), || async {
    // ... any work producing Vec<u8> ...
    Ok(b"<h1>Hello</h1>".to_vec())
}).await?;
let _ = html;
Ok(())
}

get_or_set (the CacheExt method) takes &self; get_or_set_fragment takes &dyn Cache — otherwise they are identical in behaviour (both use compute-if-missing with per-call TTL, and both are best-effort, non-atomic under concurrency).


Supported Backends

Djangors includes three built-in implementations of the Cache trait:

1. In-Memory Cache (InMemoryCache)

Powered by the moka crate under the hood, InMemoryCache provides a high-performance, thread-safe in-memory cache featuring size-based eviction.

  • Best for: Single-process deployments, dev environments, or local caching of immutable data (e.g. templates).
  • Usage:
    #![allow(unused)]
    fn main() {
    use djangors::cache::InMemoryCache;
    let cache = InMemoryCache::new(10_000); // Caps capacity at 10,000 items
    }

2. Database Cache (DatabaseCache)

Uses the application’s primary database (via a djangors_cache_entries table) to persist cache values.

  • Table Schema:
    • key: TEXT PRIMARY KEY
    • value: BYTEA (Postgres) / BLOB (SQLite)
    • expires_at: TIMESTAMPTZ
  • Best for: Distributed deployments that want to share a cache without setting up a dedicated Redis instance.
  • Usage:
    #![allow(unused)]
    fn main() {
    use djangors::cache::DatabaseCache;
    let cache = DatabaseCache::new(db.clone());
    }

3. Redis Cache (RedisCache)

Provides a fast, distributed cache backend backed by a Redis server.

  • Best for: High-traffic, highly concurrent production deployments.
  • Activation: Enabled via the redis crate feature flag.
  • Usage:
    #![allow(unused)]
    fn main() {
    use djangors::cache::RedisCache;
    let cache = RedisCache::new("redis://127.0.0.1:6379/0")?;
    }

HTTP Response Caching (CacheLayer)

Djangors offers a Tower middleware middleware layer (CacheLayer) that automates serving and recording HTTP responses.

Opt-In-Only Architecture

Unlike some frameworks that cache responses globally, CacheLayer is strictly opt-in per response.

A response is only cached if:

  1. The incoming request is a GET request.
  2. The response contains the CacheableResponse marker extension.
  3. The response does NOT contain a Set-Cookie header.

Why is this opt-in?

HTTP response caching is notoriously prone to security leaks:

  • If user-specific pages (e.g. dashboards displaying personal profiles) were cached globally, subsequent users would receive the cached data of the previous user.
  • By enforcing that responses must explicitly carry the CacheableResponse extension and lack Set-Cookie headers, Djangors prevents caching sensitive sessions, CSRF tokens, or user-specific state.

Using the Cache Middleware

To wire up response caching in your application:

#![allow(unused)]
fn main() {
use djangors::cache::{CacheLayer, CacheableResponse};
use std::sync::Arc;
use std::time::Duration;

// 1. Set up the CacheLayer with your preferred cache backend
let cache_backend = Arc::new(InMemoryCache::new(1000));
let cache_layer = CacheLayer::new(cache_backend, Some(Duration::from_secs(300)));

// 2. Mark specific responses as cacheable in your view handler
async fn my_public_view() -> Response {
    let mut response = Response::new("Hello, Cached World!".into());
    response.extensions_mut().insert(CacheableResponse);
    response
}
}