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
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:
-
UnknownIdentifierExceptionfor 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
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);
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
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);
}
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);
}
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);
}
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
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);
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
}
}
Summary
Effective caching in SAP Commerce requires understanding and tuning every layer:
- Type system cache — automatic, loaded at startup, rarely needs attention
- Region cache — the workhorse cache for entities and queries; size it based on your data volume and monitor hit rates
- FlexibleSearch cache — enable for reusable queries, disable for unique-parameter queries
- Solr cache — tune filter and query result caches based on search traffic patterns
- HTTP caching — set appropriate Cache-Control headers per resource type; never cache user-specific data
- CDN caching — cache static assets aggressively, API responses conservatively, and never cache authenticated content
- 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)