Walk through what Hibernate's READ_WRITE second-level cache strategy does to a cached entry while a transaction updates that row — what a soft lock is, what concurrent readers see — and how NONSTRICT_READ_WRITE differs.
answer
- soft lock = marker in cache, not a DB lock
- locked key = miss for readers, puts rejected
- after commit: replace if replaceable, else unlock and leave empty
- version/timestamp decides replaceability
- nonstrict = evict twice, no lock, race window remains
basics
~20 sREAD_WRITE replaces the cached entry with a soft lock before the write; while it is there readers miss the cache and hit the database and other writers cannot put. After commit the lock is swapped for the new state, or just released so the next read reloads. NONSTRICT does none of this: it simply evicts, accepting brief stale reads.
solid answer
~60 sWith **READ_WRITE**, when a transaction updates a cached entity Hibernate first *locks* the entry: the cached value is replaced by a soft-lock marker. A soft lock is not a database lock — it is a sentinel stored in the cache region meaning "a write is in flight here". While it is present, readers treat the region as a miss and load from the database, and competing writers cannot install a value. On transaction completion Hibernate calls back: on commit it may replace the lock with the new state (only if the entry is still replaceable — version or timestamp check, and no concurrent lock), otherwise it simply releases the lock so the next read repopulates from the database. Locks carry an expiry so a crashed node cannot poison a region permanently. **NONSTRICT_READ_WRITE** skips all of it: no lock, no put of the new state, just an eviction at flush and again after completion. That is cheaper, but a concurrent reader can load the pre-commit row and repopulate the cache, leaving a stale entry for a while — so use it only where a short stale window is genuinely acceptable.
code
java · 10 lines@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Account {
@Id private Long id;
private BigDecimal balance;
@Version
private long version;
}go deeper
Know that READ_WRITE marks the cached entry as locked during a write so readers go to the database, while NONSTRICT just throws the entry away.
Lay out the lock → database write → replace-or-unlock sequence and explain that the soft lock exists because the cache is not transactional.
Add the replaceability check via version or timestamp, lock expiry after a crash, and the fact that heavy write rates turn READ_WRITE into pure overhead.
Reason about it as a consistency budget: quantify acceptable staleness per entity, weigh per-write cache traffic against hit rate, and be willing to conclude that the entity should not be in the second-level cache at all.
## The problem READ_WRITE solves A second-level cache provider is usually not transactional. It has no idea whether your database transaction will commit or roll back, and it can be read by other threads at any instant. So if Hibernate naively wrote the new entity state into the cache at flush time, two things break: a rollback would leave the cache holding a value that never existed in the database (a dirty read served to everyone), and interleaved writers could install values out of order. READ_WRITE exists to get approximately **read-committed** behaviour out of a non-transactional cache. ## The soft lock A *soft lock* is just a special marker object stored in the cache region under the entity's key, in place of the cached state. It is "soft" because it is advisory and lives only in the cache — it locks nothing in the database and does not block any thread. Its meaning is: *a write against this key is in progress, so do not trust anything here.* While a soft lock occupies the key: - Readers that consult the region get the marker instead of usable state and treat it as a **cache miss**, falling through to a database SELECT. Correct, just slower. - Attempts to put a value (for example, another session finishing a load) are rejected, so a concurrent reader cannot overwrite the lock with the value it read. - A second writer locking the same key produces a *concurrent* lock; that fact is remembered, and it will prevent either writer from installing state afterwards. ## The sequence for an update 1. Hibernate dirty-checks the entity and prepares the UPDATE. 2. Before or during flush, the cache access strategy **locks** the item: the entry is swapped for the soft lock and a lock timeout is stamped on it. 3. The UPDATE goes to the database inside your transaction. 4. The transaction commits or rolls back. 5. Hibernate calls the after-completion callback. On a successful commit the strategy attempts to replace the lock with the new state — but only if the entry is still *replaceable*: no other concurrent lock intervened, and the incoming state is not older than what is there, decided by comparing the entity's `@Version` value when it has one, or by cache-entry timestamps when it does not. If any check fails, or the transaction rolled back, the strategy simply **unlocks** the entry and leaves the key empty, so the next read reloads from the database. Step 5 is the interesting one for interviews: READ_WRITE prefers to leave a hole rather than risk installing wrong data. The worst case is an extra database read, never a wrong cached value from a rolled-back transaction. Note that a versioned entity gets a more precise decision than a versionless one — another practical reason to keep a version column on cached mutable entities. ## Lock expiry If a JVM dies between step 2 and step 5, the soft lock would otherwise sit in the region forever and turn every read into a database hit. So locks are stamped with an expiry (an internal timeout, on the order of a minute in Hibernate's implementation) after which they are considered stale and the key can be repopulated normally. ## Where the cost lands Every write becomes: put lock → database write → put state or remove lock. Every read racing that write becomes a database read. Under a heavy write rate on the same keys, you pay all the cache overhead and get few hits — the region degrades into pure cost, and the honest answer is to stop caching that entity. READ_WRITE pays off for entities that are read far more often than written. ## NONSTRICT_READ_WRITE by contrast NONSTRICT does no locking. On update it **removes** the entry rather than putting the new state — at flush and again after transaction completion — so subsequent readers repopulate from the database. The double removal narrows but does not close the race: a reader that read the row *before* your commit can complete its put *after* your removal, leaving a pre-commit value cached until something evicts it. Hibernate's documentation is explicit that this strategy gives no guarantee that the cached item is the latest version, and recommends setting an expiry so any stale entry is short-lived. So the choice between the two is not "safe versus unsafe" in the abstract; it is: how bad is a few seconds of staleness for this entity, and how much per-write cache traffic do you want to pay to avoid it? Rarely-changed configuration or catalogue data survives NONSTRICT happily. Anything a user will immediately re-read and notice — balances, statuses, counters — wants READ_WRITE, or wants to be left out of the second-level cache entirely.
- After a commit under READ_WRITE, is the new state always written back into the cache?No. The strategy only installs the new state if the entry is still replaceable — no other writer soft-locked the same key concurrently, and the incoming state is not older than what is there, judged by the entity's version value or by cache-entry timestamps. Otherwise it just releases the lock and leaves the key empty, so the next read reloads from the database. Preferring a miss over a possibly-wrong value is the whole point.
- What happens to a soft lock if the node holding it crashes mid-transaction?The lock would otherwise strand the key and turn every read into a database hit. Hibernate stamps soft locks with an expiry, so after that timeout the marker is treated as stale and the key can be populated normally again. That is why you may briefly see a region with reduced hit rate after a node dies, rather than a permanently poisoned entry.
READ_WRITE puts an 'out of service' tag on the cached copy while the real ledger is updated, so everyone consults the ledger meanwhile. NONSTRICT just throws the copy away and hopes nobody re-pins an old one back on the board before the update lands.
saying these in an interview costs you the question
- Describing the soft lock as a database row lock or as something that blocks other threads
- Saying READ_WRITE always writes the post-commit value into the cache
- Claiming NONSTRICT_READ_WRITE updates the cached entry with the new value instead of evicting it
- Believing the double eviction in NONSTRICT closes the stale-read race entirely
- Recommending READ_WRITE for a hot, write-heavy entity instead of not caching it