๐Ÿ”ท Core trusted

boxlang-caching

Use this skill when implementing caching in BoxLang applications: cache providers, cachePut/cacheGet BIFs, output caching, cache regions, distributed caching with Redis or Couchbase, TTL policies, and distributed locking.

$ npx skills add ortus-boxlang/skills/boxlang-developer/caching
$ coldbox ai skills install ortus-boxlang/skills/boxlang-developer/caching
๐Ÿ”— https://skills.boxlang.io/skills/raw/ortus-boxlang/skills/boxlang-developer~caching

BoxLang Caching

Overview

BoxLang provides a flexible, pluggable caching system managed by CacheService. Multiple named cache regions can coexist, each backed by a different provider. The default in-memory cache is available immediately; Redis, Couchbase, JDBC, and filesystem caches are available via modules.

Cache Configuration

In boxlang.json

{
    "caches": {
        "default": {
            "provider": "BoxCacheProvider",
            "properties": {
                "maxObjects": 1000,
                "defaultTimeout": 60,
                "defaultLastAccessTimeout": 30,
                "reapFrequency": 5,
                "evictionPolicy": "LRU"
            }
        },
        "sessions": {
            "provider": "BoxCacheProvider",
            "properties": {
                "maxObjects": 5000,
                "defaultTimeout": 120
            }
        },
        "queries": {
            "provider": "jdbc",
            "properties": {
                "datasource": "mainDB",
                "table": "cache_entries"
            }
        },
        "files": {
            "provider": "filesystem",
            "properties": {
                "directory": "/tmp/bxcache"
            }
        }
    }
}

Basic Cache Operations

// Store a value (default cache, 60-minute TTL)
cachePut( "user_#userId#", userStruct )

// Store with explicit TTL (timespan)
cachePut( "user_#userId#", userStruct, createTimeSpan( 0, 1, 0, 0 ) )  // 1 hour

// Store with TTL and idle timeout
cachePut(
    "user_#userId#",
    userStruct,
    createTimeSpan( 0, 2, 0, 0 ),   // absolute TTL: 2 hours
    createTimeSpan( 0, 0, 30, 0 )   // idle timeout: 30 minutes
)

// Retrieve
var user = cacheGet( "user_#userId#" )
if ( isNull( user ) ) {
    user = userService.findById( userId )
    cachePut( "user_#userId#", user, createTimeSpan( 0, 1, 0, 0 ) )
}

// Check existence without fetching
if ( cacheKeyExists( "user_#userId#" ) ) {
    // ...
}

// Remove
cacheRemove( "user_#userId#" )

// Remove multiple
cacheRemove( ["user_1", "user_2", "user_3"] )

// Clear all entries in default cache
cacheClear()

Named Cache Regions

// Use a specific named cache
cachePut( "product_#id#", product, createTimeSpan(0,4,0,0), "", "products" )
var product = cacheGet( "product_#id#", false, "products" )

// Check in named cache
if ( cacheKeyExists( "product_#id#", "products" ) ) { ... }

// Clear a specific cache region
cacheClear( "products" )

// Get cache statistics
var stats = cacheGetStats( "products" )
writeOutput( "Hits: #stats.hits#, Misses: #stats.misses#, Size: #stats.size#" )

// List all cache IDs
var keys = cacheGetAllIds( "products" )

Cache-Aside Pattern

// Reusable cache-aside helper
function cacheOrLoad(
    required string key,
    required function loader,
    any ttl = createTimeSpan(0,1,0,0),
    string cacheName = "default"
) {
    var cached = cacheGet( arguments.key, false, arguments.cacheName )
    if ( !isNull( cached ) ) {
        return cached
    }
    var fresh = arguments.loader()
    cachePut( arguments.key, fresh, arguments.ttl, "", arguments.cacheName )
    return fresh
}

// Usage
var config = cacheOrLoad(
    key    = "app_config",
    loader = () -> configService.loadFromDB(),
    ttl    = createTimeSpan(0,0,15,0)  // 15 minutes
)

Output Caching

Cache rendered HTML output to avoid re-executing expensive templates:

// Cache entire block output for 10 minutes
bx:cache timespan=createTimeSpan(0,0,10,0) {
    // Expensive rendering
    var products = queryExecute("SELECT * FROM products")
    for ( var p in products ) {
        include "views/product-card.bxm"
    }
}

// With a custom cache key
bx:cache timespan=createTimeSpan(0,0,5,0) id="dashboard_#session.userId#" {
    renderDashboard()
}

// Named cache region for output
bx:cache timespan=createTimeSpan(0,1,0,0) cacheName="pages" {
    renderHomePage()
}

Redis Caching (bx-redis module)

// boxlang.json โ€” configure Redis cache
{
    "modules": {
        "bx-redis": {
            "enabled": true,
            "settings": {
                "host": "${REDIS_HOST:localhost}",
                "port": "${REDIS_PORT:6379}",
                "password": "${REDIS_PASSWORD:}",
                "database": 0
            }
        }
    },
    "caches": {
        "distributed": {
            "provider": "redis",
            "properties": {
                "defaultTimeout": 300,
                "keyPrefix": "myapp:"
            }
        }
    }
}
// Use Redis cache transparently via the same API
cachePut( "session_#token#", sessionData, createTimeSpan(0,2,0,0), "", "distributed" )
var sessionData = cacheGet( "session_#token#", false, "distributed" )

Distributed Locking

Prevent cache stampede with distributed locks:

// Only one process rebuilds the cache at a time
bx:lock name="rebuild_product_cache" type="exclusive" timeout=30 {
    // Double-check after acquiring the lock
    var cached = cacheGet( "all_products" )
    if ( isNull( cached ) ) {
        var products = queryExecute( "SELECT * FROM products" )
        cachePut( "all_products", products, createTimeSpan(0,0,30,0) )
    }
}

Couchbase (bx-couchbase module)

// boxlang.json
{
    "modules": {
        "bx-couchbase": {
            "enabled": true,
            "settings": {
                "servers": ["couchbase://localhost"],
                "bucket": "myapp",
                "username": "${COUCH_USER}",
                "password": "${COUCH_PASS}"
            }
        }
    },
    "caches": {
        "docStore": {
            "provider": "couchbase",
            "properties": {
                "defaultTimeout": 3600
            }
        }
    }
}

Eviction Policies

PolicyDescription
LRULeast Recently Used (default)
LFULeast Frequently Used
FIFOFirst-In, First-Out
RandomRandom eviction

Cache Warming on Application Start

// Application.bx
boolean function onApplicationStart() {
    // Pre-load frequently accessed data
    var config = configService.loadAll()
    cachePut( "app_config", config, createTimeSpan(1,0,0,0) )

    var categories = queryExecute( "SELECT * FROM categories" )
    cachePut( "categories", categories, createTimeSpan(0,4,0,0) )

    return true
}

Cache Invalidation Patterns

// Tag-based invalidation (group related keys)
function invalidateUserCache( required numeric userId ) {
    cacheRemove( "user_#userId#" )
    cacheRemove( "user_profile_#userId#" )
    cacheRemove( "user_permissions_#userId#" )
    cacheRemove( "user_orders_#userId#" )
}

// Versioned cache keys (avoids stampedes)
function getCacheKey( required string base ) {
    var version = cacheGet( "v:#base#" ) ?: 1
    return "#base#:v#version#"
}

function invalidateVersion( required string base ) {
    var current = cacheGet( "v:#base#" ) ?: 1
    cachePut( "v:#base#", current + 1, createTimeSpan(1,0,0,0) )
}

cacheGetOrSet โ€” Atomic Get-or-Load

cacheGetOrSet() atomically returns a cached value or executes a loader closure and stores the result if no entry exists:

// Signature: cacheGetOrSet( key, valueFunction, timeout, cacheName )
var settings = cacheGetOrSet(
    "appSettings",
    () => settingService.loadAll(),
    60  // minutes
)

// With named cache region
var product = cacheGetOrSet(
    "product_#id#",
    () => queryExecute( "SELECT * FROM products WHERE id = :id", { id: id }, { returntype: "struct" } ),
    createTimeSpan( 0, 1, 0, 0 ),
    "products"
)

Query Caching via cachedWithin

Cache query results directly in queryExecute() options:

// Cache for 1 hour โ€” result is reused on subsequent calls
var users = queryExecute(
    "SELECT * FROM users WHERE isActive = 1",
    {},
    { cachedWithin: createTimeSpan( 0, 1, 0, 0 ) }
)

// Cache with a named key (use cacheRemove() to invalidate)
var products = queryExecute(
    "SELECT * FROM products ORDER BY name",
    {},
    {
        cacheName    : "productList",
        cachedWithin : createTimeSpan( 0, 0, 30, 0 )
    }
)

// Invalidate manually when data changes
function updateProduct( required numeric id, required struct data ) {
    queryExecute( "UPDATE products SET name = :name WHERE id = :id",
                  { name: data.name, id: id } )
    cacheRemove( "productList" )
}

Function-Level Memoization

Use the cachedWithin function attribute to cache a function's return value based on its arguments:

// Method result cached per unique argument set for 1 hour
function fibonacci( required numeric n ) cachedWithin=createTimeSpan( 0, 1, 0, 0 ) {
    if ( n <= 1 ) return n
    return fibonacci( n - 1 ) + fibonacci( n - 2 )
}

// Manual in-memory memoization (no TTL)
class ExpensiveService {
    variables.memo = {}

    function compute( required numeric id ) {
        if ( memo.keyExists( id ) ) return memo[ id ]
        var result = runExpensiveCalculation( id )
        memo[ id ] = result
        return result
    }
}

References