Spring Data JPA
save vs saveAndFlush
JPA Best Practices
Spring Framework
Database Transactions

Difference between save and saveAndFlush in Spring data jpa

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

save() persists an entity to the JPA persistence context and defers the SQL INSERT or UPDATE until the transaction commits (or until JPA decides to flush automatically). saveAndFlush() does the same thing but immediately forces a flush, executing the SQL statement right away. In most cases, save() is the correct choice because it allows JPA to batch and optimize database writes. Use saveAndFlush() only when you need the database to reflect the change immediately within the same transaction.

How the Persistence Context Works

To understand the difference, you need to understand JPA's persistence context (also called the first-level cache). When you call save(), JPA does not immediately execute SQL. Instead, it marks the entity as "managed" in the persistence context and queues the change. The actual SQL is sent to the database when one of these happens:

  1. The transaction commits (most common)
  2. JPA auto-flushes before a query to ensure consistency
  3. You explicitly call flush() or use saveAndFlush()
java
1@Entity
2public class Order {
3    @Id
4    @GeneratedValue(strategy = GenerationType.IDENTITY)
5    private Long id;
6    private String product;
7    private BigDecimal amount;
8    private String status;
9
10    // constructors, getters, setters
11}

save() Behavior

java
1@Service
2@Transactional
3public class OrderService {
4
5    @Autowired
6    private OrderRepository orderRepository;
7
8    public Order createOrder(String product, BigDecimal amount) {
9        Order order = new Order();
10        order.setProduct(product);
11        order.setAmount(amount);
12        order.setStatus("PENDING");
13
14        Order saved = orderRepository.save(order);
15        // At this point:
16        // - The entity is managed in the persistence context
17        // - The ID may or may not be populated (depends on generation strategy)
18        // - NO SQL INSERT has been sent to the database yet
19
20        System.out.println("Order ID: " + saved.getId());
21        // With GenerationType.IDENTITY, the ID IS populated because
22        // JPA must execute INSERT to get the generated ID.
23        // With GenerationType.SEQUENCE, JPA fetches the next sequence
24        // value but defers the INSERT.
25
26        return saved;
27        // SQL INSERT executes when this method returns and the
28        // @Transactional proxy commits the transaction
29    }
30}

The key insight: save() makes the entity managed, but the timing of the SQL depends on the ID generation strategy and JPA's flush mode.

saveAndFlush() Behavior

java
1@Service
2@Transactional
3public class OrderService {
4
5    @Autowired
6    private OrderRepository orderRepository;
7
8    public Order createAndValidateOrder(String product, BigDecimal amount) {
9        Order order = new Order();
10        order.setProduct(product);
11        order.setAmount(amount);
12        order.setStatus("PENDING");
13
14        Order saved = orderRepository.saveAndFlush(order);
15        // At this point:
16        // - The entity is managed in the persistence context
17        // - The SQL INSERT has been executed
18        // - The ID is populated
19        // - Database constraints have been checked
20        // - But the transaction has NOT been committed yet
21
22        System.out.println("Order persisted with ID: " + saved.getId());
23        return saved;
24    }
25}

After saveAndFlush(), the SQL has been sent to the database. This means database-level constraints (unique, foreign key, check) have been validated. But the transaction is still open. If an exception occurs later, the entire transaction still rolls back.

When saveAndFlush() Is Necessary

Catching Database Constraint Violations Early

java
1@Transactional
2public Order createUniqueOrder(String product) {
3    Order order = new Order();
4    order.setProduct(product);
5    order.setStatus("PENDING");
6
7    try {
8        orderRepository.saveAndFlush(order);
9    } catch (DataIntegrityViolationException e) {
10        // Constraint violation detected immediately
11        // Without flush, this exception would only appear at commit time,
12        // possibly outside your try-catch
13        throw new DuplicateOrderException("Order for " + product + " already exists");
14    }
15
16    // Continue with more operations that depend on the order being persisted
17    auditService.logOrderCreation(order);
18    return order;
19}

Native Queries That Bypass the Persistence Context

java
1@Transactional
2public void processOrder(Long orderId) {
3    Order order = orderRepository.findById(orderId).orElseThrow();
4    order.setStatus("PROCESSING");
5    orderRepository.saveAndFlush(order);
6
7    // This native query reads directly from the database, bypassing JPA's cache.
8    // Without the flush above, it would see the old status.
9    int count = orderRepository.countProcessingOrdersNative();
10    System.out.println("Processing orders: " + count);
11}
java
// In the repository
@Query(value = "SELECT COUNT(*) FROM orders WHERE status = 'PROCESSING'", nativeQuery = true)
int countProcessingOrdersNative();

Database-Generated Values (Triggers, Defaults)

java
1@Transactional
2public Order createOrderWithDefaults() {
3    Order order = new Order();
4    order.setProduct("Widget");
5
6    // The database has a trigger that sets created_at and order_number
7    Order saved = orderRepository.saveAndFlush(order);
8
9    // Refresh to pick up trigger-generated values
10    entityManager.refresh(saved);
11
12    System.out.println("Order number: " + saved.getOrderNumber());
13    return saved;
14}

Comparison Table

Aspectsave()saveAndFlush()
SQL executionDeferred until flush/commitImmediate
Returns managed entityYesYes
ID populatedDepends on generation strategyAlways (after INSERT executes)
Constraint checkAt commit timeAt flush time (immediately)
Batch optimizationYes (JPA can batch multiple saves)No (forces immediate write)
PerformanceGenerally betterSlightly worse per call
Transaction still openYesYes (flush is not commit)
Rollback on later failureYesYes
Native query consistencyMay see stale dataDatabase reflects the change

The flush() Method Directly

saveAndFlush() is a convenience method equivalent to calling save() followed by flush(). You can also flush the entire persistence context manually:

java
1@Transactional
2public void batchProcess(List<OrderDto> dtos) {
3    for (int i = 0; i < dtos.size(); i++) {
4        Order order = new Order();
5        order.setProduct(dtos.get(i).getProduct());
6        order.setAmount(dtos.get(i).getAmount());
7        orderRepository.save(order);
8
9        // Flush and clear every 50 entities to prevent
10        // the persistence context from growing too large
11        if (i % 50 == 0) {
12            entityManager.flush();
13            entityManager.clear();
14        }
15    }
16}

This pattern is important for batch processing. Without periodic flushing and clearing, the persistence context accumulates all entities in memory, which can cause OutOfMemoryError for large batches.

saveAll() vs. Multiple save() Calls

Spring Data JPA also provides saveAll(), which iterates over a collection and calls save() on each entity. It does not batch the SQL by itself. For true batch inserts, configure Hibernate's batch size:

properties
1# application.properties
2spring.jpa.properties.hibernate.jdbc.batch_size=50
3spring.jpa.properties.hibernate.order_inserts=true
4spring.jpa.properties.hibernate.order_updates=true

With these settings, saveAll() followed by a single flush will batch the SQL into groups of 50, which is significantly faster than individual inserts.

Common Pitfalls

  • Using saveAndFlush() by default "just to be safe." This defeats JPA's batching optimization and increases the number of database round-trips. Use save() unless you have a specific reason to flush immediately.
  • Assuming saveAndFlush() commits the transaction. It does not. Flush writes SQL to the database but the transaction remains open. A later exception still causes a full rollback.
  • Calling save() and then immediately querying with a native query, expecting to see the saved data. Native queries bypass the persistence context. Either use a JPQL query (which triggers auto-flush) or call saveAndFlush() before the native query.
  • Forgetting to clear the persistence context during batch processing. Calling save() thousands of times without flush() and clear() keeps all entities in memory. Use periodic flush-and-clear for large batches.
  • Relying on saveAndFlush() to catch constraint violations when the exception type depends on the database driver. Wrap the call in a try-catch for DataIntegrityViolationException, not for driver-specific exceptions.

Summary

  • save() marks an entity as managed and defers SQL execution. It allows JPA to optimize with batching and is the right default choice.
  • saveAndFlush() marks the entity as managed and immediately executes the SQL. Use it when you need constraint validation, native query consistency, or database-generated values within the same transaction.
  • Neither save() nor saveAndFlush() commits the transaction. Rollback is still possible after both.
  • For batch processing, use save() with periodic flush() and clear() calls, combined with Hibernate's jdbc.batch_size setting.
  • Default to save(). Reach for saveAndFlush() only when deferred execution causes a specific problem.

Course illustration
Course illustration

All Rights Reserved.