skip to content

Does @Transactional(readOnly = true) prevent writes? What happens if you modify an entity inside one?

level: middleimportance: must knowfreq 65%

answer

  1. Not forbidden — silently dropped
  2. Auto-flush skipped => UPDATE never emitted
  3. flush()/@Modifying/JDBC still write
  4. Replica may throw SQL error (DB, not Spring)
  5. Advisory, not a guardrail

basics

~20 s

Usually nothing is written, but not because writes are forbidden. Because FlushMode is MANUAL, Hibernate never auto-flushes, so a dirty entity change is silently discarded at commit. It is not an error — it is a missing flush.

solid answer

~40 s

readOnly = true is not enforcement, so modifying a managed entity does not throw. What happens is subtler: Spring sets the Hibernate Session to FlushMode.MANUAL, so the automatic flush at commit is skipped and your dirty change is silently NOT persisted. That is the classic gotcha — the write appears to succeed in code but nothing hits the database. However, if you force it (session.flush() explicitly, or run a JPQL/native bulk UPDATE/DELETE, or write via plain JDBC), the change WILL be persisted, because none of those go through the skipped auto-flush. Some databases/drivers may reject writes when Connection.setReadOnly(true) actually takes effect (or on a read replica), producing a SQL error, but you cannot rely on that. So: not forbidden, just usually silently dropped.

code

java · 13 lines
java
@Transactional(readOnly = true)
public void raiseSalarySilentlyLost(Long id) {
    Employee e = employeeRepo.findById(id).orElseThrow();
    e.setSalary(e.getSalary() + 100); // entity is now dirty...
    // No exception. At commit Spring skips the auto-flush (FlushMode.MANUAL),
    // so the UPDATE is NEVER emitted. The change is silently discarded.
}

@Transactional(readOnly = true)
public void writeAnyway(Long id) {
    employeeRepo.bulkRaise(id, 100); // @Modifying JPQL UPDATE -> DOES persist,
    // because bulk queries bypass the skipped dirty-checking flush.
}

go deeper

for a junior

Know that modifying an entity usually just does nothing (no error).

for a middle

Explain WHY: FlushMode.MANUAL skips the commit flush, so the UPDATE is never emitted.

for a senior

Enumerate the bypasses (explicit flush, @Modifying, JDBC) and the replica-throws case.

for a principal

Insists real enforcement is a DB role/replica concern, not this flag.

## The misconception Many developers believe `readOnly = true` makes Spring throw if you attempt a write. It does not. The flag is advisory. ## Why writes silently vanish With Hibernate + `JpaTransactionManager`, `readOnly = true` sets the `Session` to `FlushMode.MANUAL`. Auto-flush is what normally pushes pending entity changes to the DB (before a conflicting query and at commit). With MANUAL: - You load an entity, change a field — it becomes *dirty* in the persistence context. - At commit, Spring does **not** flush (readOnly), so Hibernate never emits the UPDATE. - No error, no write. The change is discarded when the persistence context closes. This is the number-one interview gotcha: code that *looks* like it saves silently does nothing. ## Ways a write STILL happens (bypassing the skipped auto-flush) 1. **Explicit `entityManager.flush()` / `session.flush()`** — forces the SQL now, regardless of flush mode. The UPDATE runs. 2. **JPQL/HQL or native bulk `UPDATE`/`DELETE`** (`@Modifying` queries) — these execute directly against the DB and do not depend on dirty-checking flush. 3. **Plain JDBC / JdbcTemplate** on the same connection — writes directly. In all three the ORM's skipped auto-flush is irrelevant, so `readOnly` does not stop them. ## When you DO get an error If `Connection.setReadOnly(true)` is honored by the driver/DB (or you are pinned to a read replica), the database may reject the write with a SQL error (e.g. Postgres on a hot standby, or MySQL depending on config). This is database behavior, not Spring enforcement, and is not guaranteed. ## Practical guidance - Treat `readOnly = true` as documentation + optimization, not a guardrail. - If you need to *guarantee* no writes, enforce it elsewhere: a read-only DB user/role, replica routing, or explicit review — not this flag. - Never rely on it to silently "protect" data; instead never put writes in a read-only method in the first place.

  • If nothing was flushed, was the change rolled back?
    There was nothing to roll back — the UPDATE was never issued. The dirty entity simply lives in the persistence context and is discarded when it closes; the DB never saw it.
  • How would you actually enforce that a code path cannot write?
    Not via readOnly. Use a database user/role with no write grants, route to a read replica, or architecturally separate read and write services. readOnly is a hint, not authorization.

saying these in an interview costs you the question

  • readOnly=true throws an exception when you modify an entity
  • The change is written then rolled back
  • @Modifying queries are blocked inside a readOnly transaction
  • It guarantees data cannot be changed

context