Performance issues in SAP Commerce are inevitable at scale. A storefront that responds in 200ms with 100 products and 10 concurrent users becomes a sluggish 5-second experience when the catalog grows to 500,000 SKUs and traffic hits 1,000 requests per second. The difference between a fast and slow SAP Commerce deployment rarely comes down to hardware — it’s architecture, configuration, and code patterns.
This guide covers the performance optimization techniques that matter most in real-world SAP Commerce projects. Every recommendation is drawn from production experience, not theoretical benchmarks.
Before optimizing anything, understand where the platform spends its time during a typical storefront request:
Incoming HTTP Request (OCC API)
│
├── [5-15ms] Spring Controller + Security Filter Chain
├── [10-50ms] Facade Layer (converter/populator chain)
├── [20-200ms] Service Layer (business logic + FlexibleSearch queries)
├── [5-100ms] Database Queries (via JDBC)
├── [0-50ms] Cache Lookups/Misses
├── [0-500ms] Solr Queries (product search pages)
└── [5-20ms] Response Serialization (JSON)
Total: 50-900ms typical range
The biggest offenders, in order of frequency:
The most impactful optimization is often the simplest: add the right indexes. SAP Commerce generates tables from items.xml, but the default indexes are minimal.
Identify missing indexes by enabling slow query logging:
# Log queries taking longer than 500ms
db.log.sql.slow=true
db.log.sql.slow.threshold=500
Then define indexes in your items.xml:
<itemtype code="Order" autocreate="false" generate="false">
<indexes>
<index name="orderUserDateIdx">
<key attribute="user"/>
<key attribute="creationtime"/>
</index>
<index name="orderStatusIdx">
<key attribute="status"/>
</index>
</indexes>
</itemtype>
Rules of thumb for SAP Commerce indexes:
creationtime and modifiedtime for any type you query by dateAlways SELECT {pk} only. The Model layer handles attribute loading via cache:
// BAD — fetches all columns from DB
"SELECT * FROM {Product} WHERE {catalogVersion} = ?cv"
// GOOD — fetches PKs, attributes loaded from cache on access
"SELECT {pk} FROM {Product} WHERE {catalogVersion} = ?cv"
Eliminate N+1 queries. This is the single most common performance issue in SAP Commerce DAOs:
// BAD: 1 query for products + N queries for prices
List<ProductModel> products = productDao.findAll(cv);
for (ProductModel p : products) {
priceService.getPrice(p); // triggers another query
}
// GOOD: batch fetch in one query
"SELECT {p.pk}, {pr.pk} FROM {Product AS p JOIN PriceRow AS pr ON {pr.product} = {p.pk}} WHERE {p.catalogVersion} = ?cv"
Use query parameters, never string concatenation. Parameterized queries enable database query plan caching:
// Enables plan caching
query.addQueryParameter("code", productCode);
// vs. plan cache miss every time
"WHERE {code} = '" + productCode + "'"
Avoid LIKE with leading wildcards. {name} LIKE '%widget%' always triggers a full table scan. Use Solr for full-text search.
The database connection pool is a frequent bottleneck under load:
# Connection pool size — set based on (number of web threads * 2)
db.pool.maxActive=100
db.pool.maxIdle=50
db.pool.minIdle=10
# Connection validation
db.pool.testOnBorrow=true
db.pool.validationQuery=SELECT 1
# Connection timeout
db.pool.maxWait=10000
# Statement cache (significant for repeated queries)
db.pool.maxOpenPreparedStatements=200
Sizing formula: maxActive should be at least tomcat.maxThreads * 1.5 to avoid thread starvation. If you have 200 web threads, set maxActive=300.
If running on SAP HANA (standard for CCv2):
# Enable HANA-specific optimizations
db.hana.column.store=true
# Use HANA connection pooling features
db.pool.removeAbandoned=true
db.pool.removeAbandonedTimeout=300
# HANA statement routing for scale-out
db.hana.statementRouting=true
SAP Commerce uses a multi-layer caching architecture. Understanding and tuning each layer is critical.
┌─────────────────────────────────────┐
│ Application Code │
├─────────────────────────────────────┤
│ L1: Model Cache (per-session) │ ← Fastest, smallest
├─────────────────────────────────────┤
│ L2: Region Cache (CacheRegion) │ ← Shared across threads
├─────────────────────────────────────┤
│ L3: FlexibleSearch Query Cache │ ← Query result caching
├─────────────────────────────────────┤
│ L4: Database Query Cache │ ← DB-level caching
└─────────────────────────────────────┘
Region caches are the primary caching mechanism. Configure them in local.properties:
# Main cache region (type system data, models)
cache.main=500000
cache.main.eviction=LRU
cache.main.ttl=3600
# Entity cache (individual item caching)
regioncache.entityregion.size=500000
regioncache.entityregion.eviction=LRU
# Query result cache
regioncache.queriesregion.size=100000
regioncache.queriesregion.eviction=LRU
# Type system cache
regioncache.typesystemregion.size=200000
regioncache.typesystemregion.eviction=LFU
Monitor cache hit rates via HAC (Platform → Cache) or JMX:
MBean: de.hybris.platform:type=Cache,name=RegionCacheAdapter
Attributes: HitCount, MissCount, HitRate, Size, Evictions
Target hit rates:
If hit rates are below these thresholds, increase cache sizes or review your access patterns.
Use Spring’s @Cacheable for your own service methods:
@Cacheable(value = "productMetadataCache", key = "#productCode + '_' + #catalogVersion.pk")
public ProductMetadata getProductMetadata(String productCode, CatalogVersionModel catalogVersion) {
// Expensive computation or external call
return computeMetadata(productCode, catalogVersion);
}
Configure the cache in Spring:
<bean id="productMetadataCacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager">
<constructor-arg>
<list>
<value>productMetadataCache</value>
</list>
</constructor-arg>
</bean>
For distributed caching across cluster nodes, consider using the platform’s CacheRegion API or an external cache like Redis.
The hardest problem in caching. SAP Commerce handles invalidation via:
ModelService.save() and remove() invalidate the entity cache for the affected itemcacheable.invalidateCache() or clear specific cache regionsCommon pitfall: Direct SQL updates (via jdbcTemplate or raw SQL) bypass the cache invalidation mechanism. Always use ModelService for writes unless you manually handle cache invalidation.
SAP Commerce runs on embedded Tomcat. Thread pool configuration directly impacts concurrent request handling.
# Maximum number of concurrent request processing threads
tomcat.maxthreads=200
# Minimum number of threads always kept alive
tomcat.minsparethreads=25
# Maximum queue length for incoming connections
tomcat.acceptcount=100
# Connection timeout in milliseconds
tomcat.connectiontimeout=60000
# Max number of connections
tomcat.maxconnections=10000
Sizing guideline: For a typical storefront, tomcat.maxthreads should be 200-400 for production. Set too low and requests queue up. Set too high and you overwhelm the database connection pool and cause memory pressure.
Offload long-running operations to background threads:
@Resource
private TaskService taskService;
public void processLargeOrderAsync(OrderModel order) {
TaskModel task = modelService.create(TaskModel.class);
task.setRunnerBean("orderProcessingTaskRunner");
task.setContext(order.getPk().toString());
task.setExecutionDate(new Date());
modelService.save(task);
// Returns immediately — task executes in background
}
Large HTTP sessions consume memory. Minimize session data:
# Session timeout (seconds)
default.session.timeout=3600
# Restrict session size
spring.session.store-type=none
Solr powers product search and navigation. It’s often the slowest part of product listing pages.
Reduce index size by only indexing what you need:
<!-- solr.impex -->
INSERT_UPDATE SolrIndexedProperty;solrIndexedType(identifier)[unique=true];name[unique=true];type(code);sortableType(code);fieldValueProvider;facet;facetType(code);multiValue
;$solrIndexedType;name;text;;;true;MultiSelectOr;false
;$solrIndexedType;code;string;;;false;;false
;$solrIndexedType;price;double;double;productPriceValueProvider;true;MultiSelectOr;false
;$solrIndexedType;category;string;;;true;Refine;true
;$solrIndexedType;inStockFlag;boolean;;;true;MultiSelectOr;false
Don’t index attributes you don’t search or facet on. Each indexed property increases index size, rebuild time, and query complexity.
# Batch size for indexing
solrserver.default.indexer.batch.size=200
# Number of indexer threads
solrserver.default.indexer.thread.count=4
# Commit interval during indexing
solrserver.default.indexer.autocommit.maxtime=30000
Enable Solr query caching:
<!-- solrconfig.xml customization -->
<queryResultCache class="solr.LRUCache" size="512" initialSize="256" autowarmCount="128"/>
<documentCache class="solr.LRUCache" size="4096" initialSize="1024" autowarmCount="512"/>
<filterCache class="solr.LRUCache" size="512" initialSize="256" autowarmCount="128"/>
Limit facet calculations. Each facet adds processing time. On mobile, consider showing fewer facets:
searchQuery.setFacets(Arrays.asList("category", "brand", "priceRange")); // Only essential facets
// Instead of 15+ facets that desktop might show
Full reindexing is expensive. Use incremental indexing for real-time updates:
// Instead of full index, update only changed products
solrIndexerService.performIndexOperation(indexedType, IndexerOperationValues.UPDATE, productPks);
Configure a CronJob for periodic incremental indexing:
INSERT_UPDATE CronJob;code[unique=true];job(code);sessionLanguage(isocode)
;solrIncrementalIndexCronJob;solrIncrementalIndexJob;en
Catalog sync (Staged → Online) is one of the most resource-intensive operations in SAP Commerce.
Never run full synchronization in production if incremental sync is possible:
SyncConfig syncConfig = new SyncConfig();
syncConfig.setSynchronizationType(SyncConfig.INCREMENTAL);
syncConfig.setCreateSavedValues(false); // Skip audit trail for speed
syncConfig.setLogToDatabase(false); // Skip DB logging
syncConfig.setLogToFile(false); // Skip file logging
syncConfig.setForceUpdate(false); // Only sync changed items
catalogSynchronizationService.synchronize(syncItemJob, syncConfig);
# Number of sync worker threads
catalog.sync.workers=4
# Batch size for sync operations
catalog.sync.batch.size=100
# Disable unnecessary hooks during sync
catalog.sync.enable.interceptors=false
Run full syncs during off-peak hours:
INSERT_UPDATE Trigger;cronJob(code)[unique=true];cronExpression
;productCatalogSyncCronJob;0 0 3 * * ?
SAP Commerce is a memory-intensive application. JVM configuration significantly impacts performance.
# In CCv2 manifest.json or local startup scripts
# Heap size — typically 4-8GB for production
-Xms4g
-Xmx8g
# Metaspace (class metadata) — SAP Commerce loads many classes
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=1g
# Young generation sizing
-XX:NewRatio=3
For SAP Commerce workloads, G1GC is the recommended collector:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=45
# GC logging (for tuning)
-Xlog:gc*:file=/var/log/hybris/gc.log:time,uptime:filecount=10,filesize=100m
Avoid -XX:+UseConcMarkSweepGC (CMS) — it’s deprecated since Java 9 and removed in Java 14.
# String deduplication (saves memory with many duplicate strings)
-XX:+UseStringDeduplication
# Optimize for large heaps
-XX:+AlwaysPreTouch
# Thread stack size (reduce if you have many threads)
-Xss512k
# Direct memory (for NIO operations)
-XX:MaxDirectMemorySize=512m
The Populator/Converter pattern is elegant but can cause performance issues when chains are long and trigger lazy loading.
A single productConverter.convert(productModel) call might invoke 15+ populators, each accessing multiple model attributes, each potentially triggering a database query (lazy load):
productConverter.convert(product)
├── BasicProductPopulator → accesses code, name, description
├── PricePopulator → triggers price query
├── StockPopulator → triggers stock query
├── CategoryPopulator → triggers category tree traversal
├── ImagePopulator → triggers media queries
├── ReviewPopulator → triggers review query
├── PromotionPopulator → triggers promotion calculation
├── ClassificationPopulator → triggers classification query
└── ... (custom populators)
Each populator that accesses a lazy-loaded relation generates a database query. For a product listing page showing 20 products, this can mean 300+ database queries.
1. Use fieldSetLevelHelper to conditionally skip populators:
public class PricePopulator implements Populator<ProductModel, ProductData> {
@Resource
private FieldSetLevelHelper fieldSetLevelHelper;
@Override
public void populate(ProductModel source, ProductData target) {
if (fieldSetLevelHelper.isFieldIncluded("prices")) {
// Only execute expensive price logic when explicitly requested
target.setPrice(computePrice(source));
}
}
}
OCC API clients control which fields are populated via the fields query parameter:
GET /occ/v2/electronics/products/PROD-001?fields=code,name,price
2. Batch-prefetch data before conversion:
public List<ProductData> convertProducts(List<ProductModel> products) {
// Pre-fetch all prices in one query
Map<String, PriceInformation> priceMap = priceService.getPricesForProducts(products);
// Pre-fetch all stock levels in one query
Map<String, StockData> stockMap = stockService.getStockForProducts(products);
// Now convert — populators read from maps, not DB
ThreadLocalContext.setPriceMap(priceMap);
ThreadLocalContext.setStockMap(stockMap);
return productConverter.convertAll(products);
}
3. Create list-specific vs. detail-specific converters:
<!-- Lightweight converter for listing pages -->
<bean id="productListConverter" parent="abstractPopulatingConverter">
<property name="targetClass" value="com.mycompany.data.ProductData"/>
<property name="populators">
<list>
<ref bean="basicProductPopulator"/>
<ref bean="pricePopulator"/>
<ref bean="imagePopulator"/>
</list>
</property>
</bean>
<!-- Full converter for PDP (product detail page) -->
<bean id="productDetailConverter" parent="abstractPopulatingConverter">
<property name="targetClass" value="com.mycompany.data.ProductData"/>
<property name="populators">
<list>
<ref bean="basicProductPopulator"/>
<ref bean="pricePopulator"/>
<ref bean="stockPopulator"/>
<ref bean="imagePopulator"/>
<ref bean="reviewPopulator"/>
<ref bean="classificationPopulator"/>
<ref bean="promotionPopulator"/>
</list>
</property>
</bean>
You can’t optimize what you can’t measure.
HAC Performance Monitoring:
Platform → Cache — Cache hit rates and sizesMonitoring → Performance — Request timing breakdownMonitoring → Database — Query statisticsKey properties for monitoring:
# Enable performance monitoring
hac.monitoring.enabled=true
# Log slow requests (>2 seconds)
monitoring.slowrequest.threshold=2000
monitoring.slowrequest.enabled=true
# Log slow queries (>500ms)
db.log.sql.slow=true
db.log.sql.slow.threshold=500
Expose key metrics via JMX for monitoring tools (Dynatrace, Datadog, Prometheus):
# Enable JMX
tomcat.jmx.port=9999
tomcat.jmx.enabled=true
Key MBeans to monitor:
de.hybris.platform:type=Cache — cache statisticsjava.lang:type=Memory — heap usagejava.lang:type=GarbageCollector — GC pause timesde.hybris.platform:type=Cluster — cluster node healthFor production environments, integrate an APM tool:
# Dynatrace OneAgent (CCv2)
# Configured via CCv2 portal, not properties
# Generic Java agent
CATALINA_OPTS=-javaagent:/path/to/agent.jar
APM tools provide:
When running on Commerce Cloud v2, additional considerations apply.
CCv2 uses “aspects” — predefined server configurations:
{
"aspects": [
{
"name": "backoffice",
"properties": [
{ "key": "tomcat.maxthreads", "value": "100" },
{ "key": "db.pool.maxActive", "value": "50" }
]
},
{
"name": "accstorefront",
"properties": [
{ "key": "tomcat.maxthreads", "value": "400" },
{ "key": "db.pool.maxActive", "value": "200" }
]
},
{
"name": "backgroundProcessing",
"properties": [
{ "key": "tomcat.maxthreads", "value": "50" },
{ "key": "cronjob.maxthreads", "value": "8" }
]
}
]
}
Separate background processing (CronJobs, imports, sync) from storefront traffic to prevent resource contention.
CCv2 supports multiple instances per aspect. Request distribution is handled by the load balancer:
{
"aspects": [
{
"name": "accstorefront",
"properties": [],
"webapps": [
{ "name": "ycommercewebservices", "contextPath": "/occ" }
]
}
]
}
Scale storefront aspects horizontally for traffic peaks. Scale backgroundProcessing aspects for batch operations.
# Enable static resource versioning
storefront.resourceBundle.enabled=true
# Set aggressive caching for static resources
media.default.cache.control=public, max-age=31536000
Configure the CDN (Azure Front Door in CCv2) to cache:
Use this checklist before go-live:
SELECT {pk} onlymaxActive ≥ tomcat.maxthreads * 1.5)db.log.sql.slow.threshold=500)@Cacheable where appropriatefields parameter used in OCC API calls to limit dataPerformance optimization in SAP Commerce is not a one-time activity — it’s a continuous practice. The key principles:
SELECT {pk} only@Cacheable, monitor hit ratesThe best-performing SAP Commerce deployments aren’t the ones with the most hardware — they’re the ones where developers understand the platform’s internals and make informed architectural decisions from day one.