Back to Flush, Persist & Cache
Q3Question 3 of 5

What is flush vs commit?

Key Concepts
  • Flush synchronizes the persistence context with the database by executing buffered SQL, but does not end the transaction.
  • Commit performs a final flush and then issues the database COMMIT, making all changes permanent and visible to other transactions.
  • Flushed-but-uncommitted changes are still inside the transaction and can absolutely be rolled back.
  • With FlushMode.AUTO, Hibernate auto-flushes before a query that could be affected by pending changes and before commit.
  • A flush can acquire row locks and surface constraint violations early, but does not release locks -- only commit does.
  • In Spring you rarely call commit yourself; the @Transactional proxy commits on normal return and rolls back on a runtime exception.
  • flush() followed by clear() is the standard batch memory pattern, but each flush is still part of one transaction.

Definition

Flush sends SQL to the database now but keeps the transaction open; commit finalizes the transaction and makes changes permanent; flushed-but-uncommitted changes can be rolled back.
  • Flush -- synchronizes the persistence context with the database by executing buffered INSERT, UPDATE, and DELETE statements on the open JDBC connection, but does not end the transaction.
  • Commit -- performs a final flush of anything still buffered and then issues the database COMMIT, making all changes permanent and visible to other transactions.
  • Rollback safety -- changes that are flushed but not yet committed are still inside the transaction and can be rolled back; the database holds them in an uncommitted state.
  • Auto-flush -- with the default FlushMode.AUTO, Hibernate flushes before commit and before queries that could be affected by pending changes.
  • Locks -- a flush can acquire row locks; only commit releases them.

Flush is like Rajesh keying Priya's deposit into the terminal at National Bank of India so it appears on his screen and locks the amount in the day's working ledger -- but the branch has not yet closed its books. Commit is the branch manager closing the books at end of day, making every entry final and visible to the whole bank. If something goes wrong before the books close, even a keyed-in entry (flushed) can be struck out (rolled back).

Understanding the Concepts

What is a flush?

A flush is the moment Hibernate drains the persistence context's action queue. It runs dirty checking, orders the statements, and executes them on the JDBC connection. After a flush, the database knows about the changes within the current transaction, but the transaction has not ended.

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;

@Service
public class FlushDemo {

    private final EntityManager em;

    public FlushDemo(EntityManager em) {
        this.em = em;
    }

    @Transactional
    public void demonstrateFlush() {
        Account account = em.find(Account.class, "SAV2001");
        account.credit(new BigDecimal("1000.00"));

        em.flush(); // UPDATE accounts SET balance=... runs NOW, TX still open
        // The database has the new balance within the transaction
        // But other transactions cannot see it yet (isolation)
    }
}
TEXT
Expected behavior:
- em.flush() executes: UPDATE accounts SET balance=51000.00 WHERE id='SAV2001'
- Transaction is still open -- not committed
- Other connections see the OLD balance (transaction isolation)

What is a commit?

A commit does two things: it triggers a final flush of any remaining buffered changes, and then it issues the SQL COMMIT that ends the transaction. After commit, changes are durable, visible to other transactions, and cannot be rolled back.

JAVA
package com.bank.service;

import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class CommitDemo {

    private final AccountRepository accountRepo;

    public CommitDemo(AccountRepository accountRepo) {
        this.accountRepo = accountRepo;
    }

    @Transactional
    public void transferFunds(String fromId, String toId, BigDecimal amount) {
        Account from = accountRepo.findById(fromId).orElseThrow();
        Account to = accountRepo.findById(toId).orElseThrow();

        from.debit(amount);
        to.credit(amount);

        // When this method returns normally:
        // 1. Spring's @Transactional proxy calls flush (remaining changes)
        // 2. Then calls COMMIT
        // 3. Both balance changes become permanent and visible
    }
}
TEXT
Expected behavior at method return:
1. Auto-flush: UPDATE accounts SET balance=49000 WHERE id='SAV2001'
               UPDATE accounts SET balance=51000 WHERE id='SAV2002'
2. COMMIT
3. Changes are now permanent and visible to all connections

Can flushed-but-uncommitted changes be rolled back?

Yes. Between flush and commit the changes live in an uncommitted state inside the transaction. The database keeps enough information in its undo/WAL logs to reverse them. If a rollback happens, the database undoes even the flushed statements.

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;

@Service
public class RollbackAfterFlushDemo {

    private final EntityManager em;

    public RollbackAfterFlushDemo(EntityManager em) {
        this.em = em;
    }

    @Transactional
    public void demonstrateRollbackAfterFlush() {
        Account sender = em.find(Account.class, "SAV2001");
        sender.debit(new BigDecimal("5000.00"));

        em.flush(); // UPDATE runs -- debit is in the database (uncommitted)

        Account receiver = em.find(Account.class, "SAV2002");
        if (receiver.isFrozen()) {
            throw new RuntimeException("Receiver account is frozen!");
            // Spring's @Transactional catches the RuntimeException
            // and calls ROLLBACK
            // The flushed debit is UNDONE -- both balances unchanged
        }

        receiver.credit(new BigDecimal("5000.00"));
    }
}
TEXT
Expected behavior:
1. sender.debit() changes balance in memory
2. em.flush() sends UPDATE to DB -- debit is flushed but uncommitted
3. Receiver is frozen -- RuntimeException thrown
4. @Transactional proxy calls ROLLBACK
5. Database undoes the flushed debit
6. Both accounts have their ORIGINAL balances -- as if nothing happened

How does auto-flush (FlushMode.AUTO) work?

With the default FlushMode.AUTO, Hibernate flushes automatically before executing a query whose results could be affected by pending changes, and always before commit. This keeps queries consistent with in-memory changes.

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 AutoFlushDemo {

    private final EntityManager em;

    public AutoFlushDemo(EntityManager em) {
        this.em = em;
    }

    @Transactional
    public List<Account> demonstrateAutoFlush() {
        Account account = em.find(Account.class, "SAV2001");
        account.setBalance(BigDecimal.ZERO); // pending change, not flushed yet

        // AUTO flushes before this query because it touches the accounts table
        List<Account> zeroBalance = em.createQuery(
            "SELECT a FROM Account a WHERE a.balance = 0", Account.class)
            .getResultList();
        // SAV2001 appears in the results because auto-flush ran first

        return zeroBalance;
    }
}
TEXT
Expected behavior:
1. setBalance(ZERO) -- change queued, no SQL yet
2. JPQL query touches accounts table -- Hibernate detects pending change
3. AUTO flush fires: UPDATE accounts SET balance=0 WHERE id='SAV2001'
4. Query runs: SELECT * FROM accounts WHERE balance=0
5. SAV2001 is in the result set

How do flush and commit compare?

| Aspect | flush() | commit() |
|--------|---------|----------|
| Executes SQL | Yes | Yes (final flush + COMMIT) |
| Ends transaction | No | Yes |
| Changes permanent | No (uncommitted) | Yes |
| Visible to others | No (isolated) | Yes |
| Can be rolled back | Yes | No |
| Releases locks | No | Yes |
| Who calls it | Developer or Hibernate auto | @Transactional proxy |

JAVA
// Flush vs Commit in one method
@Transactional
public void demonstrateBoth() {
    Account a = em.find(Account.class, "SAV2001");
    a.setStatus("FROZEN");

    em.flush();  // SQL runs now, TX still open, locks held
    // Other TXs see the old status (isolation)
    // A rollback here would undo the flush

    // Method returns normally → commit()
    // SQL COMMIT, locks released, status visible to everyone
}
DIAGRAM
Timeline:
  flush()           commit()
    │                  │
    ▼                  ▼
  SQL runs          TX ends
  Locks held        Locks released
  Rollback OK       Permanent
  Invisible         Visible to all

What are common mistakes with flush and commit?

The most common mistake is thinking that flush is the same as commit. The second is flushing unnecessarily in a loop, defeating Hibernate's batching.

JAVA
// WRONG: thinking flush makes changes permanent
@Transactional
public void wrongAssumption() {
    Account a = em.find(Account.class, "SAV2001");
    a.setBalance(BigDecimal.ZERO);
    em.flush(); // SQL runs, but NOT permanent!
    // If an exception occurs after this line, the change is rolled back
}

// WRONG: unnecessary flush in a tight loop
@Transactional
public void wastefulFlush(List<Account> accounts) {
    for (Account a : accounts) {
        a.accrueInterest();
        em.flush(); // one flush per entity -- defeats batching!
    }
}

// CORRECT: batch flush
@Transactional
public void efficientFlush(List<Account> accounts) {
    for (int i = 0; i < accounts.size(); i++) {
        accounts.get(i).accrueInterest();
        if (i % 50 == 0 && i > 0) {
            em.flush();  // batch of 50
            em.clear();  // free memory
        }
    }
}
TEXT
Common mistakes:
1. Thinking flush = commit (flush is NOT permanent)
2. Flushing every iteration (defeats batching)
3. Not realizing flush acquires locks that commit releases
4. Forgetting that auto-flush fires before queries

How does flush interact with database locks?

A flush acquires row-level locks on the affected rows, but those locks are only released at commit. This means a long gap between flush and commit holds locks longer, which can block other transactions waiting on those rows.

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;

@Service
public class FlushLockDemo {

    private final EntityManager em;

    public FlushLockDemo(EntityManager em) {
        this.em = em;
    }

    @Transactional
    public void demonstrateFlushLock() {
        Account a = em.find(Account.class, "SAV2001");
        a.setBalance(new BigDecimal("0.00"));

        em.flush(); // UPDATE runs, row-level lock acquired on SAV2001
        // Lock is HELD until commit

        // Long computation here holds the lock...
        // Other transactions trying to update SAV2001 are BLOCKED
    }
}
TEXT
Flush lock behavior:
- flush() acquires row lock on SAV2001
- Lock held through any work between flush and commit
- Other transactions waiting on SAV2001 are blocked
- commit() releases the lock
- Keep the flush-to-commit window short!

What should a senior engineer know about flush vs commit?

A senior engineer knows that flush can surface constraint violations and lock contention early, which is useful for debugging. They know that flush acquires row locks that only commit releases, so long-running work between flush and commit holds locks. They know that in batch jobs, flush+clear is the memory pattern, but each flush is still part of one transaction that succeeds or fails as a whole.

JAVA
package com.bank.service;

import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class SeniorFlushPattern {

    private final EntityManager em;

    public SeniorFlushPattern(EntityManager em) {
        this.em = em;
    }

    @Transactional
    public void earlyConstraintDetection() {
        // Flush early to detect constraint violations at a known point
        em.persist(new com.bank.model.Account("SAV2001", "Priya", java.math.BigDecimal.ZERO));
        em.flush(); // If SAV2001 already exists, exception HERE, not at commit
    }
}
TEXT
Senior checklist:
1. flush is NOT commit -- flushed data can be rolled back
2. flush acquires locks; commit releases them
3. Auto-flush keeps queries consistent with pending changes
4. flush+clear in batch loops bounds memory but stays in one TX
5. In Spring, commit is handled by @Transactional proxy
6. Forcing flush surfaces constraint errors at a known line

Visual Overview

DIAGRAM
  Transaction Timeline: Flush vs Commit

  ──┬───────────┬───────────────────────┬──► Time
    │           │                       │
  BEGIN       flush()                commit()
    │           │                       │
    │     SQL executes              SQL COMMIT
    │     Locks acquired            Locks released
    │     Still in TX               TX ends
    │     Rollback possible         Permanent
    │     Invisible to others       Visible to all
DIAGRAM
  Rollback after flush:

  BEGIN → flush(UPDATE) → exception → ROLLBACK
    │          │              │           │
    │     SQL sent to DB   error!    DB undoes
    │     (uncommitted)              the UPDATE
    │                                │
    └────────────────────────────────┘
    Result: database unchanged, as if nothing happened

Key Takeaway

A flush synchronizes the persistence context with the database by executing buffered SQL on the open connection, but it does not end the transaction. A commit performs a final flush and then issues the database COMMIT that makes every change permanent and visible to other transactions. The single most important point for an interview is that changes which are flushed but not yet committed are still inside the transaction and can absolutely be rolled back, because the database keeps them in an uncommitted state with enough undo information to reverse them.

The reason flush exists separately from commit is visibility and timing within the same transaction. With auto-flush, Hibernate flushes before a query whose results depend on pending changes, keeping in-memory writes and query results consistent. A flush also surfaces constraint violations and lock contention early, at a known line, rather than at commit. But flush does not release locks and does not make anything durable -- only commit does that.

At National Bank of India, the transfer service debits the sender, flushes so an in-transaction fraud query sees the reduced balance, and then credits the receiver. In one case the receiver's account was frozen, so after the debit had already been flushed, a RuntimeException triggered a rollback, and the database undid the flushed debit, leaving both balances exactly as before. That proves flush is not a point of no return.

Remember one sentence: flush sends SQL to the database within the transaction but can still be rolled back; commit makes it permanent and visible.

---

Share
Use ← → arrow keys to navigate