You are deciding the Hibernate second-level cache concurrency strategy for each entity in a system: some are immutable reference tables, some change a few times a day, some are written constantly. How do you make that call per entity, and when do you decide not to cache an entity at all?
answer
- two axes: mutation rate × staleness cost
- immutable→read-only; rare→nonstrict; read-mostly mutable→read-write
- JTA-only → transactional
- write-heavy / hot keys / low reuse → don't cache
- validate with hit ratio, evictions, invalidation traffic
basics
~20 sMatch strategy to mutability and read/write ratio: immutable → READ_ONLY; rarely changed and staleness-tolerant → NONSTRICT_READ_WRITE; mutable, read-mostly, staleness-sensitive → READ_WRITE; atomic cache/DB under JTA → TRANSACTIONAL. Write-heavy or low-reuse entities should not be cached at all.
solid answer
~50 sI decide per entity along two axes: **how often it changes** and **how badly a stale read hurts**. - Never changes after insert → **READ_ONLY**. Cheapest, no invalidation traffic; reference and lookup tables. - Changes rarely, and a few seconds of staleness is harmless → **NONSTRICT_READ_WRITE**, with a modest time-to-live so any stale entry is short-lived. - Changes regularly but is read far more than written, and stale reads would be visible to users → **READ_WRITE**, ideally with a version column so post-commit replacement decisions are precise. - Cache and database must be atomic and I am already on JTA with a transactional provider → **TRANSACTIONAL**, accepting the two-phase-commit cost. And the fourth answer: **do not cache**. If an entity is written as often as it is read, every write pays lock-and-invalidate traffic and readers keep missing, so the cache is pure overhead. Same for entities with poor key reuse or huge state. I validate the choice with hit-rate and eviction metrics, not assumptions.
code
java · 14 lines@Entity @Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
class Country { }
@Entity @Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
class PricingTable { }
@Entity @Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
class Customer { @Version long version; }
@Entity
class AuditEvent { }go deeper
Be able to match the obvious cases: immutable reference data to READ_ONLY, frequently changing data probably not cached at all.
Use mutability plus read/write ratio to justify each strategy and mention pairing NONSTRICT_READ_WRITE with a short time-to-live.
Argue from measured read/write mix and region metrics, note that a version column sharpens READ_WRITE's replacement decision, and identify entities to exclude.
Treat it as a per-entity consistency and cost budget across the whole system, including cluster invalidation traffic and the architectural commitment that choosing TRANSACTIONAL imposes.
## The two axes Every sensible choice here falls out of two properties of the entity, not of the framework: 1. **Mutation rate** — how often rows change, through normal application traffic (not deployments). 2. **Staleness sensitivity** — what actually goes wrong if a user or another service reads a value that is a few seconds behind. A third, quieter factor decides whether to cache at all: **reuse**. A cache only pays if the same keys are read many times between writes. ## Mapping entities to strategies **Immutable reference data → READ_ONLY.** Currencies, countries, ISO codes, tax jurisdictions that only change with a release, immutable audit or event rows. No locking, no version bookkeeping, no invalidation messages in a cluster. When such data does change with a deployment, treat it as replacement: migrate, then evict the region or roll the nodes. This is the highest-value caching in most systems — small tables joined or looked up constantly. **Rarely mutated, staleness-tolerant → NONSTRICT_READ_WRITE.** Feature flags, pricing tables refreshed nightly, editorial content, configuration edited by an admin now and then. You accept that a concurrent reader can repopulate a pre-commit value and that the entry may be briefly stale, so pair it with a bounded time-to-live. The question to ask the business is concrete: *if someone sees the old value for ten seconds, what breaks?* If the answer is "nothing", take the cheaper strategy. **Mutable, read-mostly, staleness-visible → READ_WRITE.** Customer profiles, account records, order headers — read on many requests, written occasionally, and where showing a value a user just changed would be a visible bug. Give these entities a `@Version` column: READ_WRITE uses the version to decide whether the post-commit state may replace the soft lock, which is more precise than the timestamp fallback. Accept the extra per-write cache round-trips. **Atomicity required and JTA already present → TRANSACTIONAL.** Rare in practice. It only exists if you run a JTA transaction manager and a provider that can enlist in the transaction, and it adds two-phase-commit cost to every write. Choosing it drags an architectural commitment across the whole system, so it should be a deliberate platform decision, not a per-entity afterthought. ## When not to cache This is the answer that separates a senior from a principal response. Reasons to leave an entity out of the second-level cache entirely: - **Write rate approaches read rate.** Under READ_WRITE, every write costs a lock put plus a state put or a removal, and readers racing the write miss anyway. You pay full price for a low hit rate. - **Hot keys under concurrent writers.** Repeated concurrent soft locks on the same key mean post-commit replacement keeps being refused, so the entry stays empty and every read hits the database — the cache does nothing but add work. - **Poor reuse.** Entities read once per key (event rows, per-request records) evict each other and give near-zero hit rate while consuming memory. - **Large state or wide graphs.** Memory pressure and serialization cost in a distributed provider can outweigh the saved query. - **Strict freshness requirements.** If the domain says a value must never be stale — a balance used to authorise a payment — the right control is a database read (with the appropriate locking at that layer), not a cache tuned to be nearly fresh. In a clustered deployment, add the operational cost: every write on a cached mutable entity generates invalidation traffic to every node. A modestly cached, heavily written entity can make a cluster slower than no cache at all. ## How to decide rather than guess Start from the data, not the annotations. Rank entities by read volume and by write volume (query logs, statistics, application metrics). Cache the top-read, low-write entities first; leave the rest alone. Then verify: watch hit ratio, miss ratio, eviction and put counts per region. A region with a low hit ratio or eviction count close to put count is a region that should be shrunk, retuned, or removed. Do the same after each change in traffic shape — a strategy chosen for last year's read/write mix is not automatically right for this year's. Finally, keep the choice explicit per entity in the mapping rather than relying on a global default: a reader of the code should be able to see the consistency contract for that entity without knowing the container's configuration.
- An entity is read constantly but also written constantly by many threads. What do you do?Leave it out of the second-level cache. Under READ_WRITE, concurrent writers keep soft-locking the same key, post-commit replacement is repeatedly refused, and readers fall through to the database anyway — so you pay lock and invalidation traffic for almost no hits. If the read path really needs relief, address it outside the ORM cache: a narrower projection, a better index, or an explicit application-level cache with semantics you control.
- How would you know afterwards that a caching choice was wrong?Instrument the regions and read the numbers: hit ratio, miss count, put count, eviction count and, in a cluster, invalidation message volume. A region whose evictions approach its puts, or whose hit ratio is low, is churn rather than caching. Rising write rate on a READ_WRITE entity showing up as latency on the write path is the other classic signal, and both should trigger revisiting the strategy or dropping the entity from the cache.
saying these in an interview costs you the question
- Annotating every entity as cacheable and calling it an optimisation
- Defaulting everything to READ_WRITE because it 'sounds safest'
- Never considering that not caching is the right answer for write-heavy entities
- Ignoring cluster invalidation traffic when judging the cost of caching mutable entities
- Choosing TRANSACTIONAL without a JTA transaction manager or a provider that supports it