home / skills / salesforcecommercecloud / b2c-developer-tooling / b2c-custom-caches

This skill helps you implement and manage B2C custom caches with CacheMgr to boost performance and control cache lifecycles.

npx playbooks add skill salesforcecommercecloud/b2c-developer-tooling --skill b2c-custom-caches

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.9 KB
---
name: b2c-custom-caches
description: Implement custom caching with CacheMgr in B2C Commerce. Use when adding application-level caching, cache invalidation, or optimizing performance with custom cache regions. Covers cache definition JSON, CacheMgr API, and cache entry lifecycle.
---

# B2C Custom Caches

Custom caches improve code performance by storing data that is expensive to calculate, takes a long time to retrieve, or is accessed frequently. Caches are defined in JSON files within cartridges and accessed via the Script API.

## When to Use Custom Caches

| Use Case | Example |
|----------|---------|
| Expensive calculations | Check if any variation product is on sale for base product display |
| External system responses | Cache in-store availability or prices from external APIs |
| Configuration settings | Store configuration data from JSON files or external sources |
| Frequently accessed data | Product attributes, category data, site preferences |

## Limitations

| Constraint | Value |
|------------|-------|
| Total memory per app server | ~20 MB for all custom caches |
| Max caches per code version | 100 |
| Max entry size | 128 KB |
| Supported value types | Primitives, arrays, plain objects, `null` (not `undefined`) |
| Cross-server sync | None (caches are per-application-server) |

## Defining a Custom Cache

### File Structure

```
my_cartridge/
├── package.json       # References caches.json
└── caches.json        # Cache definitions
```

### package.json

Add a `caches` entry pointing to the cache definition file:

```json
{
  "name": "my_cartridge",
  "caches": "./caches.json"
}
```

### caches.json

Define caches with unique IDs and optional expiration:

```json
{
  "caches": [
    {
      "id": "ProductAttributeCache"
    },
    {
      "id": "ExternalPriceCache",
      "expireAfterSeconds": 300
    },
    {
      "id": "SiteConfigCache",
      "expireAfterSeconds": 60
    }
  ]
}
```

| Property | Required | Description |
|----------|----------|-------------|
| `id` | Yes | Unique ID across all cartridges in code version |
| `expireAfterSeconds` | No | Maximum seconds an entry is retained |

## Using Custom Caches

### Script API Classes

| Class | Description |
|-------|-------------|
| `dw.system.CacheMgr` | Entry point for accessing defined caches |
| `dw.system.Cache` | Cache instance for storing and retrieving entries |

### Basic Usage

```javascript
var CacheMgr = require('dw/system/CacheMgr');

// Get a defined cache
var cache = CacheMgr.getCache('ProductAttributeCache');

// Get value (returns undefined if not found)
var value = cache.get('myKey');

// Store value directly
cache.put('myKey', { data: 'value' });

// Remove entry
cache.invalidate('myKey');
```

### Recommended Pattern: get with Loader

Use `get(key, loader)` to automatically populate the cache on miss:

```javascript
var CacheMgr = require('dw/system/CacheMgr');
var Site = require('dw/system/Site');

var cache = CacheMgr.getCache('SiteConfigCache');

// Loader function called only on cache miss
var config = cache.get(Site.current.ID + '_config', function() {
    // Expensive operation - only runs if not cached
    return loadConfigurationFromFile(Site.current);
});
```

### Scoped Cache Keys

Include scope identifiers in keys to separate entries by context:

```javascript
var CacheMgr = require('dw/system/CacheMgr');
var Site = require('dw/system/Site');

var cache = CacheMgr.getCache('ProductCache');

// Site-scoped key
var siteKey = Site.current.ID + '_' + productID;
var productData = cache.get(siteKey, loadProductData);

// Catalog-scoped key
var catalogKey = 'catalog_' + catalogID + '_' + productID;
var catalogData = cache.get(catalogKey, loadCatalogData);

// Locale-scoped key
var localeKey = request.locale + '_' + contentID;
var content = cache.get(localeKey, loadLocalizedContent);
```

## Cache Methods

| Method | Description |
|--------|-------------|
| `get(key)` | Returns cached value or `undefined` |
| `get(key, loader)` | Returns cached value or calls loader, stores result |
| `put(key, value)` | Stores value directly (overwrites existing) |
| `invalidate(key)` | Removes entry for key |

## Best Practices

### Do

- Use `get(key, loader)` pattern for automatic population
- Include scope (site, catalog, locale) in cache keys
- Set appropriate `expireAfterSeconds` for time-sensitive data
- Handle cache misses gracefully (data may be evicted anytime)
- Use descriptive cache IDs

### Don't

- Include personal user data in cache keys (keys may appear in logs)
- Store Script API objects (only primitives and plain objects)
- Rely on cache entries existing (no persistence guarantee)
- Expect cross-server cache synchronization
- Store `undefined` values (use `null` instead)

## Cache Invalidation

Caches are automatically cleared when:
- Any file in the active code version changes
- A new code version is activated
- Data replication completes
- Code replication completes

Manual invalidation only affects the current application server:

```javascript
var cache = CacheMgr.getCache('MyCache');

// Invalidate single entry (current app server only)
cache.invalidate('myKey');

// Storing undefined has same effect as invalidate
cache.put('myKey', undefined);
```

## Common Patterns

### Caching External API Responses

```javascript
var CacheMgr = require('dw/system/CacheMgr');
var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry');

var priceCache = CacheMgr.getCache('ExternalPriceCache');

function getExternalPrice(productID) {
    return priceCache.get('price_' + productID, function() {
        var service = LocalServiceRegistry.createService('PriceService', {
            createRequest: function(svc, args) {
                svc.setRequestMethod('GET');
                svc.addParam('productId', args.productID);
                return null;
            },
            parseResponse: function(svc, response) {
                return JSON.parse(response.text);
            }
        });

        var result = service.call({ productID: productID });
        return result.ok ? result.object : null;
    });
}
```

### Caching Expensive Calculations

```javascript
var CacheMgr = require('dw/system/CacheMgr');

var saleCache = CacheMgr.getCache('ProductSaleCache');

function isProductOnSale(masterProduct) {
    return saleCache.get('sale_' + masterProduct.ID, function() {
        var variants = masterProduct.variants.iterator();
        while (variants.hasNext()) {
            var variant = variants.next();
            if (isInPromotion(variant)) {
                return true;
            }
        }
        return false;
    });
}
```

### Configuration Cache with Site Scope

```javascript
var CacheMgr = require('dw/system/CacheMgr');
var Site = require('dw/system/Site');
var File = require('dw/io/File');
var FileReader = require('dw/io/FileReader');

var configCache = CacheMgr.getCache('SiteConfigCache');

function getSiteConfig() {
    var siteID = Site.current.ID;

    return configCache.get(siteID + '_config', function() {
        var configFile = new File(File.IMPEX + '/src/config/' + siteID + '.json');
        if (!configFile.exists()) {
            return null;
        }

        var reader = new FileReader(configFile);
        var content = reader.getString();
        reader.close();

        return JSON.parse(content);
    });
}
```

## Troubleshooting

| Issue | Cause | Solution |
|-------|-------|----------|
| Cache not found exception | Cache ID not defined in any caches.json | Add cache definition to caches.json |
| Duplicate cache ID error | Same ID used in multiple cartridges | Use unique IDs across all cartridges |
| Entry not stored | Value exceeds 128 KB limit | Reduce data size or cache subsets |
| Entry not stored | Value contains Script API objects | Use only primitives and plain objects |
| Unexpected cache misses | Different app server or cache cleared | Always handle misses gracefully |

Check the custom error log and custom warn log for cache-related messages.

Overview

This skill documents how to implement custom caching with CacheMgr in B2C Commerce. It explains cache definition, usage patterns, entry lifecycle, and invalidation to help you add application-level caches and optimize performance. The content focuses on practical examples, limits, and common pitfalls.

How this skill works

Caches are declared in a cartridge caches.json and wired via package.json. The platform exposes dw.system.CacheMgr to retrieve named Cache instances that store simple values (primitives, arrays, plain objects, null). Use cache.get(key, loader) to populate on miss, cache.put(key, value) to store, and cache.invalidate(key) to remove entries. Caches live per application server and are bounded by size and entry limits.

When to use it

  • Cache expensive calculations or aggregated results (e.g., sale checks across variants)
  • Cache responses from external services (prices, availability) to reduce API calls
  • Store frequently accessed configuration or site preference data
  • Improve read latency for product attributes or category data
  • Reduce repeated heavy file parsing or DB-like work at runtime

Best practices

  • Define caches in caches.json and reference it from package.json for each cartridge
  • Prefer cache.get(key, loader) so loaders run only on misses and reduce race conditions
  • Include scope identifiers (site, catalog, locale) in keys to avoid collisions
  • Set expireAfterSeconds for time-sensitive data and handle misses gracefully
  • Never cache Script API objects; use primitives or plain JSON-serializable objects
  • Avoid including personal user data in keys and do not store undefined (use null)

Example use cases

  • Cache external price service responses per product with a short TTL
  • Cache computed boolean for whether a product is on sale to speed listing renders
  • Cache site-specific configuration JSON and invalidate when code or files change
  • Cache localized content per locale key to reduce content assembly cost
  • Cache catalog-scoped product metadata keyed by catalogID + productID

FAQ

What are common causes of cache entries not being stored?

Entries exceed the ~128 KB size limit or include Script API objects. Reduce payload size or store plain objects only.

Are caches shared across application servers?

No. Caches are per application server and do not synchronize across servers; handle misses as expected.