Definition
- AUTO (default) -- flushes before the transaction commits and before executing a query whose results could be affected by pending changes, keeping queries consistent with in-memory state.
- COMMIT -- flushes only at commit time, so a query during the transaction may miss unflushed changes and return stale data.
- MANUAL -- flushes only when you explicitly call
flush(), giving full control at the cost of remembering to do it. - ALWAYS -- flushes before every query unconditionally, even if no pending changes exist for that query's tables.
- Table detection -- AUTO inspects each query's target tables against the action queue; COMMIT and MANUAL skip this check entirely.
Think of the persistence context as Rajesh's working tray of unprocessed slips at National Bank of India. AUTO means: before answering any customer question that could depend on those slips, and before closing the counter, Rajesh keys them all in first -- so answers are always current. COMMIT means Rajesh only keys slips in at counter-close, so if a customer asks a balance mid-day it might ignore slips still in the tray. MANUAL means Rajesh keys slips in only when Pankaj explicitly says "process the tray now."
Understanding the Concepts
What is FlushMode.AUTO?
AUTO is the default flush mode. Before running a query, Hibernate checks whether the query touches tables that have pending inserts, updates, or deletes in the action queue. If so, it flushes first so the query returns correct, up-to-date results. It also always flushes before commit.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class AutoFlushModeDemo {
private final EntityManager em;
public AutoFlushModeDemo(EntityManager em) {
this.em = em;
}
@Transactional
public List<Account> demonstrateAuto() {
Account account = em.find(Account.class, "SAV2001");
account.setBalance(BigDecimal.ZERO); // pending change
// AUTO detects that this query touches the accounts table
// which has a pending UPDATE, so it flushes first
List<Account> zeroBalance = em.createQuery(
"SELECT a FROM Account a WHERE a.balance = 0", Account.class)
.getResultList();
// SAV2001 is in the result because AUTO flushed before the query
return zeroBalance;
}
}
Expected behavior with AUTO:
1. setBalance(ZERO) -- change queued, no SQL yet
2. JPQL query on accounts table -- AUTO detects pending change
3. Hibernate flushes: UPDATE accounts SET balance=0 WHERE id='SAV2001'
4. Query runs: SELECT * FROM accounts WHERE balance=0
5. SAV2001 appears in the results -- consistent!
What is FlushMode.COMMIT?
COMMIT mode flushes only at commit time, not before queries. A query may run against stale database state and miss unflushed changes. Use it when you deliberately do not want query-triggered flushes.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class CommitFlushModeDemo {
private final EntityManager em;
public CommitFlushModeDemo(EntityManager em) {
this.em = em;
}
@Transactional
public List<Account> demonstrateCommit() {
em.setFlushMode(FlushModeType.COMMIT);
Account account = em.find(Account.class, "SAV2001");
account.setBalance(BigDecimal.ZERO); // pending change
// COMMIT mode: NO flush before the query
List<Account> zeroBalance = em.createQuery(
"SELECT a FROM Account a WHERE a.balance = 0", Account.class)
.getResultList();
// SAV2001 may NOT be in the results!
// The database still has the old balance
return zeroBalance;
}
}
Expected behavior with COMMIT:
1. setBalance(ZERO) -- change queued
2. JPQL query -- NO flush before the query
3. Query runs against the database's OLD balance
4. SAV2001 NOT in the results -- stale read!
5. At commit: UPDATE runs, then COMMIT finalizes
What is FlushMode.MANUAL?
MANUAL mode flushes only when you explicitly call flush(). Nothing flushes automatically -- not even before commit in the Hibernate-native sense. This gives full control at the cost of discipline.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import org.hibernate.Session;
import org.hibernate.FlushMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class ManualFlushModeDemo {
private final EntityManager em;
public ManualFlushModeDemo(EntityManager em) {
this.em = em;
}
@Transactional
public void demonstrateManual() {
Session session = em.unwrap(Session.class);
session.setHibernateFlushMode(FlushMode.MANUAL);
Account account = em.find(Account.class, "SAV2001");
account.setBalance(new BigDecimal("100000.00"));
// No auto-flush before queries, not even before commit
// Changes silently lost if you forget to call flush!
session.flush(); // ONLY way to push changes in MANUAL mode
}
}
Expected behavior with MANUAL:
1. setBalance(100000) -- change queued
2. Any query runs WITHOUT flushing
3. If you forget session.flush() -- changes NEVER reach the database!
4. MANUAL requires explicit flush() calls -- discipline required
How do the flush modes compare?
| Flush Mode | Flushes before query? | Flushes at commit? | Risk | Use Case |
|------------|----------------------|-------------------|------|----------|
| AUTO | Yes (if tables affected) | Yes | None (consistent) | Default for most work |
| COMMIT | No | Yes | Stale reads within TX | Read-heavy paths |
| MANUAL | No | No (Hibernate-native) | Lost changes | Advanced conversations |
| ALWAYS | Yes (all queries) | Yes | Over-flushing | Debugging |
// Setting flush mode per session
em.setFlushMode(FlushModeType.COMMIT); // JPA standard
// Setting flush mode per query
var query = em.createQuery("SELECT a FROM Account a", Account.class);
query.setFlushMode(FlushModeType.COMMIT); // only this query skips auto-flush
// Hibernate-native MANUAL
Session session = em.unwrap(Session.class);
session.setHibernateFlushMode(FlushMode.MANUAL);
Decision guide:
- Normal transactional work? --> AUTO (default, safest)
- Read-heavy path with minor pending changes? --> COMMIT (fewer flushes)
- Full control over when SQL runs? --> MANUAL (advanced use only)
- Debugging flush behavior? --> ALWAYS (flush before every query)
How does AUTO detect which queries need a flush?
AUTO inspects the query's target tables (from the JPQL or HQL parsing) and checks if the action queue has pending changes for those tables. If there is an overlap, it flushes. If the query touches a different table with no pending changes, it skips the flush.
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.AuditLog;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class SelectiveAutoFlushDemo {
private final EntityManager em;
public SelectiveAutoFlushDemo(EntityManager em) {
this.em = em;
}
@Transactional
public void demonstrateSelectiveFlush() {
Account account = em.find(Account.class, "SAV2001");
account.setBalance(BigDecimal.ZERO); // pending change on accounts table
// Query on audit_logs table -- NO pending changes for that table
// AUTO does NOT flush here (smart -- accounts table is irrelevant)
List<AuditLog> logs = em.createQuery(
"SELECT l FROM AuditLog l", AuditLog.class).getResultList();
// Query on accounts table -- pending changes exist
// AUTO DOES flush here
List<Account> accounts = em.createQuery(
"SELECT a FROM Account a", Account.class).getResultList();
}
}
Expected behavior:
1. Pending change on accounts table
2. Query on audit_logs -- NO flush (different table)
3. Query on accounts -- AUTO flush fires (same table has pending change)
What about native SQL queries and flush modes?
Native SQL queries do not participate reliably in AUTO's table-dirtiness detection because Hibernate cannot parse raw SQL to determine which tables are involved. As a safety measure, Hibernate may flush everything before a native query, or you may need an explicit flush.
package com.bank.service;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class NativeQueryFlushDemo {
private final EntityManager em;
public NativeQueryFlushDemo(EntityManager em) {
this.em = em;
}
@Transactional
public void demonstrateNativeQueryFlush() {
var account = em.find(com.bank.model.Account.class, "SAV2001");
account.setBalance(BigDecimal.ZERO);
// Native query -- Hibernate cannot parse which tables are involved
// May flush everything or may miss the pending change
em.flush(); // explicit flush before native query is the safe pattern
Object count = em.createNativeQuery(
"SELECT COUNT(*) FROM accounts WHERE balance = 0")
.getSingleResult();
}
}
Best practice:
- JPQL/HQL queries: trust AUTO flush detection
- Native SQL queries: call em.flush() explicitly before the query
- This ensures the native query sees all pending changes
What are common mistakes with flush modes?
The most common mistake is switching to COMMIT mode without understanding the stale-read tradeoff, leading to subtle bugs where a query does not see your own pending writes.
// WRONG: COMMIT mode causes stale read
@Transactional
public boolean isAccountFrozen(String id) {
em.setFlushMode(FlushModeType.COMMIT);
Account a = em.find(Account.class, id);
a.setStatus("FROZEN"); // pending change
// Query does NOT see the pending change!
Long frozenCount = em.createQuery(
"SELECT COUNT(a) FROM Account a WHERE a.status = 'FROZEN'", Long.class)
.getSingleResult();
return frozenCount > 0; // may return false even though we just froze it!
}
// CORRECT: use AUTO or flush explicitly before the query
@Transactional
public boolean isAccountFrozenCorrect(String id) {
Account a = em.find(Account.class, id);
a.setStatus("FROZEN");
// AUTO flush mode -- Hibernate flushes before this query
Long frozenCount = em.createQuery(
"SELECT COUNT(a) FROM Account a WHERE a.status = 'FROZEN'", Long.class)
.getSingleResult();
return frozenCount > 0; // correct: AUTO flushed the change first
}
Common mistakes:
1. COMMIT mode causing stale reads within the same transaction
2. MANUAL mode with forgotten flush() -- changes silently lost
3. Assuming native queries trigger AUTO flush reliably
4. Over-flushing with ALWAYS mode in production (performance hit)
What should a senior engineer know about flush modes?
A senior engineer uses AUTO as the safe default, switches to COMMIT only on well-understood read-heavy paths, and avoids MANUAL unless building a multi-request conversation. They always flush explicitly before native queries and understand that choosing COMMIT is a conscious tradeoff of consistency for performance.
package com.bank.service;
import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class SeniorFlushModePattern {
private final EntityManager em;
public SeniorFlushModePattern(EntityManager em) {
this.em = em;
}
// Read-heavy dashboard: COMMIT mode reduces unnecessary flushes
@Transactional(readOnly = true)
public List<?> dashboardQuery() {
// readOnly + COMMIT: no dirty checking, no auto-flush before queries
return em.createQuery("SELECT a FROM Account a WHERE a.status = 'ACTIVE'")
.setFlushMode(FlushModeType.COMMIT)
.getResultList();
}
}
Senior checklist:
1. AUTO is the safe default -- queries always consistent
2. COMMIT reduces flushes on read-heavy paths -- accept stale-read risk
3. MANUAL requires discipline -- forgotten flush = lost changes
4. Always flush explicitly before native SQL queries
5. Per-query flush mode overrides are possible and fine-grained
6. readOnly + COMMIT is the optimal read path (no snapshots, no flushes)
Visual Overview
Flush Mode Decision Flow
Query about to run
│
▼
┌──────────────────────────────┐
│ What is the flush mode? │
└────────┬─────┬────────┬──────┘
│ │ │
AUTO COMMIT MANUAL
│ │ │
▼ │ ▼
┌──────────┐ │ ┌──────────┐
│ Pending │ │ │ No flush │
│ changes │ │ │ ever │
│ on this │ │ │ (manual │
│ table? │ │ │ only) │
└──┬───┬───┘ │ └──────────┘
Yes No │
│ │ │
▼ ▼ ▼
FLUSH skip No flush
then flush until
query query commit
AUTO behavior:
pending change on ACCOUNTS table
│
Query on AUDIT_LOGS ──► No flush (different table)
Query on ACCOUNTS ──► AUTO flush fires, then query
Key Takeaway
The flush mode controls when Hibernate automatically drains the persistence context to the database. The default, AUTO, flushes before commit and before running a query whose results could be affected by pending changes, so inside a transaction your queries always stay consistent with your in-memory writes. COMMIT mode flushes only at commit, so a query may run against stale database state and miss changes you have made but not yet flushed. MANUAL mode flushes only when you explicitly call flush(), giving complete control at the cost of remembering to do it.
AUTO is the safe default because Hibernate inspects each query's target tables and checks whether the action queue has pending changes for those tables. If there is overlap, it flushes first. That is why setting an account's balance to zero and then querying for zero-balance accounts returns the correct result even without an explicit flush. COMMIT mode trades that guarantee for fewer flushes, which helps on read-heavy paths.
At National Bank of India, Rajesh used AUTO on the correction path where an adjustment updates a balance and a summary query must include the corrected figure. On a dashboard that ran many read queries in one transaction, AUTO was flushing before every read and slowing things down, so he switched that read-only path to COMMIT flush mode and removed the extra flushes. For native SQL queries he always calls flush() explicitly because native queries do not reliably participate in AUTO's table detection.
Remember one sentence: AUTO flushes before affected queries for consistency, COMMIT flushes only at commit for performance, and MANUAL gives full control but demands discipline.
---