DEV Community

Aliaksandr Tsviatkou
Aliaksandr Tsviatkou

Posted on

Caching Strategies in SAP Commerce Cloud: From Type System to CDN

Caching is the single most impactful performance lever in SAP Commerce. A well-configured caching strategy can reduce page load times by 10x, cut database load by 90%, and let a single cluster handle traffic spikes that would otherwise require emergency scaling. A poorly configured one can serve stale data, cause cache stampedes, and create debugging nightmares.

This article covers every caching layer in the SAP Commerce stack — from the type system cache and region cache through to HTTP caching and CDN configuration — with practical configuration examples and troubleshooting guidance.

Type System Cache

The type system cache holds the metadata about all item types, attributes, relations, and enumerations. It's loaded at startup and stays in memory.

How It Works

When SAP Commerce starts, it reads the entire type system definition from the database and builds an in-memory representation. Every modelService.get(), every FlexibleSearch query, every ImpEx import uses this cache to understand the data model.

Configuration

# But you can control the startup behavior:

# Force type system rebuild on startup (use after items.xml changes)
typesystem.cache.validate=true

# Log type system cache statistics
log4j2.logger.typesystem.name = de.hybris.platform.persistence.type
log4j2.logger.typesystem.level = DEBUG
Enter fullscreen mode Exit fullscreen mode

When It Causes Problems

The type system cache only causes issues during development when you change items.xml and forget to run ant updatesystem. Symptoms include:

  • UnknownIdentifierException for types you just added
  • Missing attributes on existing types
  • Stale enum values

Fix: Always run ant updatesystem after changing items.xml, then restart.

FlexibleSearch Query Cache

FlexibleSearch queries are cached at two levels: the query plan cache and the result cache.

Query Plan Cache

Compiled query plans are cached to avoid re-parsing SQL on every execution:

# Query plan cache (parsed SQL → compiled query)
flexiblesearch.cache.size=10000
flexiblesearch.cache.enabled=true
Enter fullscreen mode Exit fullscreen mode

Result Cache

Query results can be cached based on the query string and parameters:

// This query's results are cached
FlexibleSearchQuery query = new FlexibleSearchQuery(
    "SELECT {pk} FROM {Product} WHERE {code} = ?code");
query.addQueryParameter("code", "CAM-001");
query.setCacheable(true);  // Enable result caching for this query

SearchResult<ProductModel> result = flexibleSearchService.search(query);
Enter fullscreen mode Exit fullscreen mode

When Query Cache Hurts

The query cache stores complete result sets. For queries that return large result sets or queries that are rarely repeated with the same parameters, caching wastes memory:

// BAD: Don't cache queries with unique parameters
FlexibleSearchQuery query = new FlexibleSearchQuery(
    "SELECT {pk} FROM {Order} WHERE {code} = ?code");
query.addQueryParameter("code", uniqueOrderCode);
query.setCacheable(true);  // Wastes cache space — each order code is unique

// GOOD: Cache queries with reusable parameters
FlexibleSearchQuery query = new FlexibleSearchQuery(
    "SELECT {pk} FROM {Product} WHERE {approvalStatus} = ?status AND {catalogVersion} = ?cv");
query.addQueryParameter("status", ApprovalStatus.APPROVED);
query.addQueryParameter("cv", onlineCatalogVersion);
query.setCacheable(true);  // Good — this query is reused frequently
Enter fullscreen mode Exit fullscreen mode

HTTP-Level Caching

OCC API Response Caching

For the headless storefront (Spartacus), cache OCC API responses at the HTTP layer:

@GetMapping("/products/{productCode}")
public ResponseEntity<ProductWsDTO> getProduct(
        @PathVariable String productCode,
        @RequestParam(defaultValue = DEFAULT_FIELD_SET) String fields) {

    ProductData data = productFacade.getProductForCode(productCode);
    ProductWsDTO dto = dataMapper.map(data, ProductWsDTO.class, fields);

    return ResponseEntity.ok()
        .cacheControl(CacheControl
            .maxAge(5, TimeUnit.MINUTES)
            .staleWhileRevalidate(30, TimeUnit.SECONDS))
        .eTag(generateETag(dto))
        .body(dto);
}
Enter fullscreen mode Exit fullscreen mode

Cache-Control Headers by Resource Type

Resource Cache-Control Rationale
Product detail max-age=300, stale-while-revalidate=30 Changes infrequently
Product list/search max-age=60 Moderate change frequency
Cart no-store User-specific, never cache
Checkout no-store User-specific, never cache
Static assets (JS, CSS) max-age=31536000, immutable Content-hashed filenames
Product images max-age=86400 Rarely change
CMS content max-age=600, stale-while-revalidate=60 Updated by business users

Conditional Requests (ETags)

ETags enable efficient cache revalidation:

@GetMapping("/categories/{categoryCode}/products")
public ResponseEntity<ProductSearchPageWsDTO> searchProducts(
        @PathVariable String categoryCode,
        HttpServletRequest request) {

    // Generate ETag from content hash
    ProductSearchPageData results = searchFacade.categorySearch(categoryCode);
    String etag = DigestUtils.md5Hex(results.hashCode() + "");

    // Check If-None-Match header
    if (etag.equals(request.getHeader("If-None-Match"))) {
        return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
    }

    ProductSearchPageWsDTO dto = dataMapper.map(results, ProductSearchPageWsDTO.class);
    return ResponseEntity.ok()
        .eTag(etag)
        .cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES))
        .body(dto);
}
Enter fullscreen mode Exit fullscreen mode

Cache Anti-Patterns

1. Caching User-Specific Data

// WRONG: This caches per-user data with a shared key
@Cacheable("priceCache")
public PriceData getPrice(String productCode) {
    // Returns user-specific price (based on contract, user group, etc.)
    return priceFacade.getPrice(productCode);
}

// RIGHT: Include user-identifying information in cache key
@Cacheable(value = "priceCache", key = "#productCode + '-' + #userGroup")
public PriceData getPrice(String productCode, String userGroup) {
    return priceFacade.getPrice(productCode);
}
Enter fullscreen mode Exit fullscreen mode

2. Unbounded Cache Growth

# WRONG: No size limit
regioncache.entityCacheRegion.size=0

# RIGHT: Set appropriate limits based on available memory
# Rule of thumb: entity cache size ≈ unique items accessed in a typical hour
regioncache.entityCacheRegion.size=100000
Enter fullscreen mode Exit fullscreen mode

3. Too-Long TTL on Dynamic Content

// WRONG: Caching stock levels for an hour
return ResponseEntity.ok()
    .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
    .body(stockData);

// RIGHT: Short TTL or no cache for volatile data
return ResponseEntity.ok()
    .cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS))
    .body(stockData);
Enter fullscreen mode Exit fullscreen mode

4. Cache Stampede

When a cached item expires and many requests arrive simultaneously, they all miss the cache and hit the database at once.

// Solution: stale-while-revalidate
return ResponseEntity.ok()
    .cacheControl(CacheControl
        .maxAge(5, TimeUnit.MINUTES)
        .staleWhileRevalidate(60, TimeUnit.SECONDS))  // Serve stale while refreshing
    .body(dto);

// Solution: Cache warming
@Scheduled(fixedRate = 240000)  // Every 4 minutes (before 5-min TTL expires)
public void warmProductCache() {
    List<String> topProductCodes = analyticsService.getTopProductCodes(100);
    for (String code : topProductCodes) {
        productFacade.getProductForCode(code);  // Refreshes cache
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Effective caching in SAP Commerce requires understanding and tuning every layer:

  1. Type system cache — automatic, loaded at startup, rarely needs attention
  2. Region cache — the workhorse cache for entities and queries; size it based on your data volume and monitor hit rates
  3. FlexibleSearch cache — enable for reusable queries, disable for unique-parameter queries
  4. Solr cache — tune filter and query result caches based on search traffic patterns
  5. HTTP caching — set appropriate Cache-Control headers per resource type; never cache user-specific data
  6. CDN caching — cache static assets aggressively, API responses conservatively, and never cache authenticated content
  7. Monitor continuously — cache performance degrades as data grows and traffic patterns change

The goal isn't maximum caching — it's the right caching. Cache the data that's expensive to compute and frequently requested, with TTLs that balance freshness against performance. And always have a way to invalidate when the source data changes.

Top comments (0)