Persistence and Caching in SAP Commerce: Region Caches, the Type Cache, and CCv2 Sizing
How the persistence layer actually serves a request: the cache hierarchy from entity and query regions to the type cache, invalidation across the cluster, heap-relative sizing with SpEL for CCv2, and the measurement loop that keeps hit ratios honest.
Between your service layer call and the database sits a cache hierarchy that decides most of your storefront's latency. When it works, ninety-something percent of item reads never touch SQL. When it is mis-sized (and copied-from-2016 configuration usually is), the symptoms are oblique: rising DB load, GC pressure, p95s that degrade through the day and recover after restarts. This guide walks the hierarchy, the invalidation model, and the sizing discipline that CCv2's variable node sizes force on you.
The Hierarchy, Bottom Up#
The entity region caches hydrated item state by PK. Every modelService.get(), every FlexibleSearch result row that becomes a model, every lazy relation traversal lands here first. Its hit ratio is the single most consequential number in the persistence layer; on a healthy catalog-heavy storefront it should sit in the high nineties.
The query region caches FlexibleSearch result sets (the PK lists, not the hydrated items) keyed by query text plus parameters plus session context that affects visibility (catalog versions, restrictions, language). Two properties of that key matter in practice: a query that embeds volatile values (timestamps, random sampling, per-user data) is uncacheable by construction, and a query whose results depend on session state caches per-context, multiplying its footprint. The FlexibleSearch guide covers writing cache-friendly queries; the architecture-level rule is that the query region rewards a small vocabulary of parameterized queries and punishes dynamically assembled SQL-ish string soup.
The type cache holds the type system metadata (composed types, attribute descriptors) and is effectively read-only at runtime. It becomes interesting exactly once per release: after a system update that changes items.xml, when stale metadata on a node is a deployment-ordering bug, not a tuning problem.
Above the platform: whatever you add (CMS cache, Solr result caching at the search layer, CDN for full pages and media). Those layers earn their keep by keeping requests away from Tomcat entirely, but they do not excuse a sick region cache, because personalized and transactional flows always fall through to the platform.
Invalidation: Correct by Default, Costly When Abused#
Every write to an item broadcasts an invalidation for its PK (and affected query regions) across the cluster; each node evicts and re-reads lazily. You do not configure this and you cannot break it by accident from the outside. You can, however, abuse it from the inside:
- Mass writes are mass invalidations. A nightly job that touches every product does not just cost DB writes; it flushes the entity region's working set on every node and the morning's first customers repay the cache misses. This is one more argument for the data maintenance guide's principle of writing only what changed:
PDTROW_MARK_PRODUCT_MODIFIED-style flags and hash-comparison importers exist to keep no-op writes out of the persistence layer. - Catalog sync is the biggest invalidation event you run on purpose. Schedule it accordingly (the catalog synchronization guide covers the mechanics); a full sync at 09:00 on a trading day is self-harm.
- Cluster-wide eviction on deployment is normal. Every node starts cold. Warm-up (hitting key pages and queries before a node takes traffic) is what keeps rolling deployments invisible to customers.
Sizing on CCv2: Relative or Wrong#
On-premise you sized caches once against a heap you controlled. On CCv2 the build process controls the JVM, node sizes differ by environment and aspect, and any absolute maxEntries value is wrong somewhere. The pattern from the CX Works architecture material is still the correct move: derive sizes from the runtime with SpEL, calibrated against a reference heap.
<!-- 100k entries per 4GB of max heap, scaling linearly with the actual JVM -->
<bean name="ehCacheEntityCacheRegion"
class="de.hybris.platform.regioncache.region.impl.EHCacheRegion" lazy-init="true">
<constructor-arg name="name" value="entityCacheRegion"/>
<constructor-arg name="maxEntries"
value="#{100000 * ( T(java.lang.Runtime).runtime.maxMemory() / ( 1024 * 1024 * 1024 * 4.0 ) )}"/>
<constructor-arg name="evictionPolicy" value="${regioncache.entityregion.evictionpolicy}"/>
<constructor-arg name="statsEnabled" value="${regioncache.stats.enabled}"/>
<constructor-arg name="exclusiveComputation" value="${regioncache.exclusivecomputation}"/>
<property name="handledTypes">
<array>
<value>__ALL_TYPES__</value>
<value>__NO_QUERY__</value>
</array>
</property>
</bean>
Calibration notes that separate this from cargo-culting:
- The reference ratio (here 100k per 4 GB) comes from measuring your average entity size, which depends on your type system's width and localization. Measure on production-shaped data; a catalog with 40 localized attributes per product is a different animal than a lean B2C model.
- Cache entries live in heap alongside everything else. Oversizing trades DB reads for GC pauses, and GC pauses hit every request while a DB read hits one. The failure mode of an oversized cache is subtler and worse than an undersized one.
- Dedicated regions for hot types (price rows, media, CMS components) let you protect them from eviction pressure caused by bulk scans over cold types. The region-per-concern pattern beats one giant region for the same total memory.
The Measurement Loop#
Region cache statistics (hAC exposes them; Dynatrace and the platform's metrics surface them for dashboards) give you hit ratio, fill level, and eviction rate per region. The operating loop:
- Baseline on production traffic: hit ratio, evictions/minute, and fill percentage per region, captured in the same dashboard as DB load so correlation is visible.
- Alert on trend, not threshold: a region that drifts from 97% to 91% over two weeks is a catalog growth signal; the day it hits your alert threshold is late.
- After every catalog-shape change (new type, big assortment onboarding, localization additions), re-check fill and eviction within the first trading week.
- Correlate deploys with cold-start cost: if the post-deployment DB spike hurts, invest in warm-up, not bigger caches.
A region at 100% fill with climbing evictions and a sagging hit ratio wants growth (if heap headroom exists) or a workload fix (if it does not). A region at 30% fill is heap you allocated to nothing; give it back.
Transactions Intersect Here#
Two persistence-layer behaviors bite teams who only think in caches. First, reads inside a transaction that has written to an item see the transaction's state, but other nodes see the old state until commit plus invalidation; code that expects cross-node read-your-writes mid-transaction is wrong by design. Second, long transactions holding row locks while doing remote calls (the classic checkout-calls-tax-service-inside-tx pattern) turn cache-miss latency into lock-wait latency for everyone else. The transactions and locking guide picks this thread up in detail; the architectural rule is: transactions short, remote calls outside, caches doing the heavy lifting for reads.
Checklist#
- Entity and query region hit ratios on a dashboard next to DB load; trend alerts, not just thresholds
- All region sizes heap-relative (SpEL), calibrated against measured entity sizes on production-shaped data
- Hot types isolated into dedicated regions where eviction pressure demands it
- Bulk jobs and imports audited for no-op writes; catalog sync scheduled off-peak
- Deployment warm-up in place if post-deploy DB spikes are visible in the graphs
- Uncacheable queries (volatile values in query text) hunted down in code review
The persistence layer rewards exactly one habit: measuring before and after every change. Every number in this guide is a starting point; your catalog, your type system, and your traffic shape decide the finals.