Back to Flush, Persist & Cache
Q5Question 5 of 5

What are Hibernate flush modes?

Key Concepts
  • AUTO (default) flushes before queries that could be affected by pending changes and before commit, keeping queries consistent.
  • COMMIT flushes only at commit, so queries may miss unflushed changes within the same transaction.
  • MANUAL flushes only when you explicitly call flush(), giving full control but requiring discipline.
  • ALWAYS flushes before every query unconditionally, regardless of whether the query's tables have pending changes.
  • AUTO inspects each query to check if its tables have pending changes; if so, it flushes first.
  • Native SQL queries do not reliably participate in AUTO's table detection, so Hibernate may flush everything before them.
  • Choose COMMIT for read-heavy paths that hold minor pending changes, accepting the tradeoff of potentially stale reads.

Definition

AUTO flushes before affected queries and at commit; COMMIT flushes only at commit; MANUAL flushes only on explicit `flush()` calls.
  • 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.

JAVA
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;
    }
}
TEXT
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.

JAVA
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;
    }
}
TEXT
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.

JAVA
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
    }
}
TEXT
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 |

JAVA
// 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);
TEXT
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.

JAVA
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();
    }
}
TEXT
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.

JAVA
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();
    }
}
TEXT
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.

JAVA
// 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
}
TEXT
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.

JAVA
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();
    }
}
TEXT
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

DIAGRAM
  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
DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate