Skip to content

Latest commit

 

History

History
607 lines (475 loc) · 23.9 KB

File metadata and controls

607 lines (475 loc) · 23.9 KB
name cachebox-standalone
description Use this skill when working with CacheBox as a standalone caching framework (outside ColdBox) -- installing, creating and bootstrapping the CacheFactory, configuring the DSL, choosing object stores and eviction policies, selecting cache providers (CacheBoxProvider, CF, Lucee), implementing cache-aside/stampede-protection patterns, registering standalone listeners, named caches, disk/JDBC stores, reaping, shutdown, or monitoring cache performance.

CacheBox — Standalone Framework

When to Use This Skill

Use this skill when using CacheBox outside of a ColdBox application — as a pure standalone caching library. For ColdBox-integrated caching (WireBox injection, event/view caching, @cacheable), use the coldbox-cache-integration skill instead.

Language Mode Reference

Examples use CFML script unless noted. For BoxLang replace component with class and use cachebox.system or coldbox.system depending on build.


Core Concepts

CacheBox is an enterprise-grade, standalone caching aggregator for ColdFusion/BoxLang that:

  • Aggregates multiple named cache providers under one API
  • Ships built-in storage backends: RAM (ConcurrentStore), memory-sensitive (ConcurrentSoftReferenceStore), Disk, and JDBC
  • Provides pluggable eviction policies: LRU, LFU, FIFO, LIFO, custom
  • Broadcasts a rich event model through cache listeners
  • Runs embedded in ColdBox or as a pure standalone library

Namespace: Standalone = cachebox.system.*; ColdBox-embedded = coldbox.system.*


1. Installation (Standalone)

box install cachebox

Map the installed folder in Application.cfc:

this.mappings[ "/cachebox" ] = expandPath( "/path/to/cachebox" );

If using CacheBox inside ColdBox, skip installation — it is already included.


2. Modes of Operation

Mode Who creates CacheBox Note
Standalone You — in onApplicationStart Persist in application scope or WireBox
ColdBox embedded ColdBox framework Use coldbox-cache-integration skill

3. Creating the CacheFactory

3a. Zero config (default settings)

application.cacheBox = new cachebox.system.cache.CacheFactory();
var cache = application.cacheBox.getDefaultCache();

cache.set( "myKey", { name : "Luis" }, 60, 20 ); // 60 min TTL, 20 min idle
var obj = cache.get( "myKey" );

3b. Portable CFC config (recommended for production)

Create config/CacheBox.cfc:

component {

    function configure() {
        cacheBox = {
            // Used only in standalone mode for console/file logging
            logBoxConfig : "cachebox.system.cache.config.LogBox",

            // Auto-registers the factory in application scope as application.cacheBox
            scopeRegistration : {
                enabled : true,
                scope   : "application",
                key     : "cacheBox"
            },

            // MANDATORY. Name "default" is reserved; provider cannot be changed.
            // All timeouts are in MINUTES.
            defaultCache : {
                objectDefaultTimeout           : 60,
                objectDefaultLastAccessTimeout : 30,
                useLastAccessTimeouts          : true,
                reapFrequency                  : 2,
                freeMemoryPercentageThreshold  : 0,    // 0 = no JVM memory check
                evictionPolicy                 : "LRU",
                evictCount                     : 1,
                maxObjects                     : 300,
                objectStore                    : "ConcurrentSoftReferenceStore",
                coldboxEnabled                 : false, // MUST be false in standalone mode
                resetTimeoutOnAccess           : false
            },

            // Named caches — add as many domains as needed
            caches : {
                // Fast, guaranteed in-memory cache for database queries
                queryCache : {
                    provider   : "cachebox.system.cache.providers.CacheBoxProvider",
                    properties : {
                        objectDefaultTimeout           : 30,
                        objectDefaultLastAccessTimeout : 15,
                        maxObjects                     : 500,
                        objectStore                    : "ConcurrentStore" // guaranteed — no JVM eviction
                    }
                },
                // Memory-sensitive view/fragment cache
                viewCache : {
                    provider   : "cachebox.system.cache.providers.CacheBoxProvider",
                    properties : {
                        objectDefaultTimeout           : 120,
                        objectDefaultLastAccessTimeout : 30,
                        maxObjects                     : 200,
                        objectStore                    : "ConcurrentSoftReferenceStore"
                    }
                },
                // Disk-backed cache (survives app restarts; serialises complex objects)
                diskCache : {
                    provider   : "cachebox.system.cache.providers.CacheBoxProvider",
                    properties : {
                        objectDefaultTimeout : 1440,   // 24 hours
                        maxObjects           : 1000,
                        objectStore          : "DiskStore",
                        directoryPath        : "/var/cache/myapp",
                        autoExpandPath       : false
                    }
                },
                // JDBC-backed cache (small clusters; stores in a DB table)
                dbCache : {
                    provider   : "cachebox.system.cache.providers.CacheBoxProvider",
                    properties : {
                        objectDefaultTimeout : 60,
                        maxObjects           : 1000,
                        objectStore          : "JDBCStore",
                        dsn                  : "myDatasource",
                        table                : "cachebox_objects",
                        tableAutoCreate      : true
                    }
                }
            },

            // Standalone listeners — executed in declaration order
            listeners : [
                { class : "myapp.listeners.CacheLogger", name : "CacheLogger", properties : {} }
            ]
        };
    }
}

Bootstrap in Application.cfc:

component {

    this.name    = "MyApp";
    // Required standalone mapping
    this.mappings[ "/cachebox" ] = expandPath( "/lib/cachebox" );
    this.mappings[ "/myapp" ]    = expandPath( "/" );

    function onApplicationStart() {
        var config = new cachebox.system.cache.config.CacheBoxConfig(
            CFCConfigPath : "config.CacheBox"
        );
        // scopeRegistration in config auto-sets application.cacheBox
        new cachebox.system.cache.CacheFactory( config );
    }

    function onRequestStart() {
        // REQUIRED in standalone mode — triggers expiry sweep per reapFrequency
        application.cacheBox.reapAll();
    }

    function onApplicationEnd( applicationScope ) {
        if ( structKeyExists( applicationScope, "cacheBox" ) ) {
            applicationScope.cacheBox.shutdown();
        }
    }
}

3c. Programmatic config

var config = new cachebox.system.cache.config.CacheBoxConfig()
    .scopeRegistration( true, "application", "cacheBox" )
    .defaultCache(
        maxObjects     : 200,
        objectStore    : "ConcurrentStore",
        evictionPolicy : "LRU"
    )
    .cache(
        "queryCache",
        "cachebox.system.cache.providers.CacheBoxProvider",
        { objectDefaultTimeout : 30, maxObjects : 500, objectStore : "ConcurrentStore" }
    )
    .listener( "myapp.listeners.CacheLogger", {}, "CacheLogger" );

new cachebox.system.cache.CacheFactory( config );

3d. Struct literal (inline, quick scripts)

new cachebox.system.cache.CacheFactory( {
    scopeRegistration : { enabled : true, scope : "application", key : "cacheBox" },
    defaultCache : {
        objectDefaultTimeout : 60,
        maxObjects           : 300,
        objectStore          : "ConcurrentSoftReferenceStore",
        coldboxEnabled       : false
    },
    caches    : {},
    listeners : []
} );

4. Accessing the Factory and Caches

// From application scope (set by scopeRegistration)
var cacheBox = application.cacheBox;

// Always-available default cache
var cache = cacheBox.getDefaultCache();

// Named cache
var queryCache = cacheBox.getCache( "queryCache" );

// All registered cache names
var names = cacheBox.getCacheNames(); // array

// Add a default-type cache at runtime
var extra = cacheBox.addDefaultCache( "SessionCache" );

5. Core Cache Provider API

All providers share the ICacheProvider API. Timeouts are in minutes.

// ── WRITE ────────────────────────────────────────────────────────────────────
// set( key, value [, timeout [, lastAccessTimeout]] )
cache.set( "product_1", product, 60, 30 );  // 60 min TTL, 30 min idle
cache.set( "product_1", product );          // use configured defaults
cache.set( "config_data", data, 0, 0 );    // eternal (lives until explicit clear)

// ── READ ─────────────────────────────────────────────────────────────────────
var val = cache.get( "product_1" );         // null if missing or expired; resets idle timer
var val = cache.getQuiet( "product_1" );    // null if missing or expired; does NOT reset idle timer

// ── CHECK ────────────────────────────────────────────────────────────────────
var exists = cache.lookup( "product_1" );       // true/false, respects expiry, resets idle
var exists = cache.lookupQuiet( "product_1" );  // true/false, no idle reset

// ── GET-OR-SET ────────────────────────────────────────────────────────────────
var product = cache.getOrSet(
    objectKey  : "product_1",
    produce    : function() { return loadProductFromDB( 1 ) },
    timeout    : 60,
    lastAccess : 30
);

// ── METADATA ─────────────────────────────────────────────────────────────────
var meta = cache.getCachedObjectMetadata( "product_1" );
// meta.timeout, meta.lastAccessTimeout, meta.hits, meta.lastAccessed, meta.isExpired

// ── DELETE ───────────────────────────────────────────────────────────────────
cache.clear( "product_1" );                          // single key
cache.clearByKeySnippet( "product_", false );        // all keys containing "product_"
cache.clearByKeySnippet( "^product_\d+$", true );   // regex match
cache.clearAll();                                    // all keys in this provider
cache.expireObject( "product_1" );                  // lazy expire (removed on next reap)
cache.expireAll();                                   // lazy expire entire cache

// ── STATS ────────────────────────────────────────────────────────────────────
var keys  = cache.getKeys();    // array of all live keys
var size  = cache.getSize();    // count of live objects
var stats = cache.getStats();   // struct: hits, misses, evictions, performance

6. Cache Patterns

Simple cache-aside

function getProduct( required numeric id ) {
    var key     = "product_#id#"
    var product = cache.get( key )

    if ( !isNull( product ) ) {
        return product
    }

    product = productGateway.findById( id )
    cache.set( key, product, 60, 30 )
    return product
}

getOrSet() (preferred, eliminates null-check boilerplate)

function getProduct( required numeric id ) {
    return cache.getOrSet(
        objectKey  : "product_#id#",
        produce    : function() { return productGateway.findById( id ) },
        timeout    : 60,
        lastAccess : 30
    )
}

Stampede protection (double-checked locking)

Use under high concurrency when the loader is expensive:

function getWithStampedeProtection( required string key, required any loader, numeric timeout = 60 ) {
    // Fast path — no lock
    var value = cache.get( key )
    if ( !isNull( value ) ) { return value }

    // Slow path — only one thread loads; others wait, then read from cache
    lock name="cache_load_#key#" timeout="15" throwontimeout="false" type="exclusive" {
        value = cache.get( key )  // re-check inside lock
        if ( isNull( value ) ) {
            value = loader()
            cache.set( key, value, timeout )
        }
    }
    return value
}

Write-through

function save( required struct entity ) {
    var saved = repository.save( entity )
    cache.set( "entity_#saved.id#", saved, 60, 30 )
    return saved
}

function delete( required numeric id ) {
    repository.delete( id )
    cache.clear( "entity_#id#" )
    cache.clearByKeySnippet( "entity_list_" )  // invalidate related list caches
}

Cache warming on startup

// myapp/listeners/CacheWarmer.cfc (standalone listener)
component {

    function configure( required any cacheBox, required struct properties ) {
        variables.cacheBox = arguments.cacheBox;
    }

    function afterCacheFactoryConfiguration( interceptData ) {
        var cache    = variables.cacheBox.getCache( "queryCache" )
        var products = new myapp.gateways.ProductGateway().findAllActive()
        for ( var p in products ) {
            cache.set( "product_#p.id#", p, 240 )
        }
    }
}

7. Cache Providers Reference

Provider Use when
cachebox.system.cache.providers.CacheBoxProvider Standalone apps — native CacheBox engine
cachebox.system.cache.providers.CacheBoxColdBoxProvider ColdBox apps (default cache only, coldboxEnabled:true)
cachebox.system.cache.providers.CFProvider Delegate to Adobe CF built-in cache (standalone)
cachebox.system.cache.providers.CFColdBoxProvider Delegate to Adobe CF built-in cache (ColdBox)
cachebox.system.cache.providers.LuceeProvider Delegate to Lucee built-in cache (standalone)
cachebox.system.cache.providers.LuceeColdBoxProvider Delegate to Lucee built-in cache (ColdBox)
cachebox.system.cache.providers.MockProvider Testing / no-op

Always use CacheBoxProvider in standalone apps unless delegating to the CFML engine cache.


8. Object Stores Reference

Object stores define where CacheBoxProvider physically stores objects:

Store Retention Best For
ConcurrentStore Guaranteed until evicted/expired/cleared General data, queries — objects won't disappear under memory pressure
ConcurrentSoftReferenceStore JVM may collect under heap pressure View/fragment caching; memory-sensitive use cases
DiskStore Survives restarts; disk I/O cost Large payloads, cross-restart persistence
JDBCStore Database-backed; survives restarts Small clusters needing shared/centralised cache

DiskStore extra properties

Property Type Required Default Description
directoryPath string Path to store files
autoExpandPath boolean true Expand relative paths with expandPath()

JDBCStore extra properties

Property Type Required Default Description
dsn string Datasource name
table string Table name
dsnUsername string DSN username
dsnPassword string DSN password
tableAutoCreate boolean true Auto-create table if missing

ACF + JDBCStore inside <cftransaction>: All DB tags must use identical dsn/username/password. Omit them and rely on this.datasource in Application.cfc to avoid the "Usernames and Passwords must be the same" error.


9. Eviction Policies

Policy Algorithm Use When
LRU (default) Evicts least recently accessed General-purpose
LFU Evicts least frequently accessed Hot-spot key sets
FIFO Evicts oldest inserted Queue semantics
LIFO Evicts newest inserted Stack semantics
Custom path Your IEvictionPolicy impl Special requirements

Eternal objects (timeout = 0) are never evicted by policy — they live until explicit clear() or factory shutdown().


10. Reaping (Standalone Mandatory)

CacheBox does not self-reap in standalone mode. Trigger on every request:

function onRequestStart() {
    // Internally throttled to reapFrequency — safe to call every request
    application.cacheBox.reapAll();
}

11. CacheFactory Management Methods

var cacheBox = application.cacheBox;

cacheBox.addCache( myCustomCacheInstance );              // register a pre-built provider
cacheBox.addDefaultCache( "TempCache" );                // add default-type cache by name
cacheBox.removeCache( "TempCache" );                   // unregister and shut down a cache
cacheBox.replaceCache( original, decorated );           // swap with a decorated instance
cacheBox.expireAll();                                   // lazy-expire everything in all caches
cacheBox.clearAll();                                    // immediately clear all caches
cacheBox.reapAll();                                     // sweep all caches for expired objects
cacheBox.shutdown();                                    // graceful shutdown (always call on app stop)
cacheBox.getCacheNames();                               // array of registered cache names
cacheBox.isCacheRegistered( "queryCache" );             // boolean

12. Standalone Cache Listeners

Listeners are CFCs with methods named after the events they handle. Order of execution = declaration order.

// myapp/listeners/CacheLogger.cfc
component {

    // standalone configure signature — receives the factory + properties
    function configure( required any cacheBox, required struct properties ) {
        variables.cacheBox = arguments.cacheBox;
        variables.log      = arguments.cacheBox.getLogBox().getLogger( this );
    }

    // ── CacheFactory events ────────────────────────────────────────────────
    function afterCacheFactoryConfiguration( interceptData ) {
        variables.log.info( "CacheBox ready. Caches: #variables.cacheBox.getCacheNames().toList()#" );
    }

    function beforeCacheFactoryShutdown( interceptData ) {
        variables.log.warn( "CacheBox shutting down" );
    }

    // ── CacheBoxProvider events ────────────────────────────────────────────
    function afterCacheElementInsert( interceptData ) {
        variables.log.debug(
            "INSERT [#interceptData.cache.getName()#] #interceptData.cacheObjectKey# ttl=#interceptData.cacheObjectTimeout#"
        );
    }

    function afterCacheElementRemoved( interceptData ) {
        variables.log.debug(
            "REMOVE [#interceptData.cache.getName()#] #interceptData.cacheObjectKey#"
        );
    }

    function afterCacheElementExpired( interceptData ) {
        variables.log.debug(
            "EXPIRE [#interceptData.cache.getName()#] #interceptData.cacheObjectKey#"
        );
    }

    function afterCacheClearAll( interceptData ) {
        variables.log.info( "CLEAR ALL [#interceptData.cache.getName()#]" );
    }
}

CacheFactory event reference

Event interceptData keys When fired
afterCacheFactoryConfiguration cacheFactory After factory is fully started
beforeCacheFactoryShutdown cacheFactory Before graceful shutdown
afterCacheFactoryShutdown cacheFactory After shutdown completes
afterCacheRegistration cache After addCache()
beforeCacheRemoval cache Before removeCache()
afterCacheRemoval cache (name string) After cache removed
beforeCacheReplacement oldCache, newCache Before replaceCache()
beforeCacheShutdown cache Before individual cache shutdown
afterCacheShutdown cache After individual cache shutdown

CacheBoxProvider event reference

Event interceptData keys When fired
afterCacheElementInsert cache, cacheObject, cacheObjectKey, cacheObjectTimeout, cacheObjectLastAccessTimeout After set() inserts a new key
afterCacheElementUpdated cache, cacheNewObject, cacheOldObject After set() replaces existing key
afterCacheElementRemoved cache, cacheObjectKey After clear()
afterCacheElementExpired cache, cacheObjectKey Reaper found expired key
afterCacheClearAll cache After clearAll()

13. Monitoring & Stats

var cache = application.cacheBox.getDefaultCache();

// Performance stats struct
var stats = cache.getStats();
// stats.hits, stats.misses, stats.objectsAdded, stats.evictions, etc.

// Inspect a key
var meta = cache.getCachedObjectMetadata( "product_1" );
// meta.timeout, meta.lastAccessTimeout, meta.hits, meta.lastAccessed, meta.isExpired

// Inventory
var keys = cache.getKeys();
var size = cache.getSize();

14. Testing (MockProvider)

Replace a real provider with a no-op mock during tests:

function beforeAll() {
    var cacheBox  = application.cacheBox;
    var mockCache = new cachebox.system.cache.providers.MockProvider();
    mockCache.setName( "default" );
    mockCache.setConfiguration( {} );
    mockCache.init( cacheBox );
    cacheBox.replaceCache( cacheBox.getDefaultCache(), mockCache );
}

15. Production Best Practices

  • Always call shutdown() in onApplicationEnd — ensures disk/JDBC stores flush cleanly.
  • Always call reapAll() in onRequestStart — mandatory in standalone mode.
  • Persist in application scope via scopeRegistration or explicit assignment; never re-instantiate per request.
  • Use ConcurrentStore when objects must survive JVM memory pressure. Use ConcurrentSoftReferenceStore for view/template caches where graceful eviction under memory pressure is acceptable.
  • DiskStore / JDBCStore require Java-serialisable objects — structs, arrays, and simple scalars are safe; CFC instances must be serialisable.
  • Namespace all cache keys — prefix with domain and identifier ("product_#id#", "user_#id#_prefs") to avoid collisions across teams/modules.
  • Separate caches by data domain — different TTLs, sizes, and eviction policies per domain (queries vs. views vs. config).
  • Use getOrSet() to eliminate null-check boilerplate and reduce stampede risk.
  • Use clearByKeySnippet() over clearAll() when invalidating a group — avoid mass cache stampedes.
  • Eternal objects (timeout=0) are never evicted by policy — limit to true app-lifecycle singletons (e.g., reference lookup tables).
  • Monitor getStats() — low hit rates indicate key naming or TTL problems, not a need to increase maxObjects.
  • Order listeners intentionally — they run in declaration order; put cache-warming listeners before auditing ones.