Definition
- 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.
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)
}
}
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.
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
}
}
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.
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"));
}
}
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.
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;
}
}
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 |
// 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
}
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.
// 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
}
}
}
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.
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
}
}
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.
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
}
}
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
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
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.
---