Spring Boot cookbook · Intermediate

Handle concurrent updates with optimistic locking

Prevent lost updates when two users edit the same record, and retry automatically for background updates.

Approach

Add @Version to the entity. For user edits, return 409/412 and let the user reload (use ETags with the version). For automated updates (counters, stock), retry the transaction a few times on ObjectOptimisticLockingFailureException — from a separate bean so each attempt is a new transaction.

Solution

@Entity
class Inventory {
    @Id private String sku;
    private int available;
    @Version private long version;

    void reserve(int qty) {
        if (available < qty) throw new InsufficientStock(sku, available);
        available -= qty;
    }
}

@Service
class InventoryService {
    private final InventoryRepository repo;
    InventoryService(InventoryRepository repo) { this.repo = repo; }

    @Transactional
    public void reserve(String sku, int qty) {
        repo.findById(sku).orElseThrow().reserve(qty);
    }
}

@Service
class ReservationFacade {
    private final InventoryService inventory;
    ReservationFacade(InventoryService inventory) { this.inventory = inventory; }

    public void reserveWithRetry(String sku, int qty) {
        for (int attempt = 1; ; attempt++) {
            try {
                inventory.reserve(sku, qty);            // new transaction per attempt (through the proxy)
                return;
            } catch (ObjectOptimisticLockingFailureException e) {
                if (attempt == 3) throw e;
                try { Thread.sleep(ThreadLocalRandom.current().nextLong(20, 100) * attempt); }
                catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw e; }
            }
        }
    }
}

Pitfalls

  • Retrying inside the same transaction (the persistence context is stale).
  • Silently overwriting user edits instead of surfacing conflicts.
  • High-contention hot rows — consider atomic SQL updates instead.

Variations

  • @Retryable (Spring Retry, or Spring Framework 7's built-in resilience annotations) on the facade method.
  • Atomic update: UPDATE inventory SET available = available - :q WHERE sku = :s AND available >= :q.

Further reading: Spring Data JPA: Persisting entities

Reference: https://docs.spring.io/spring-data/jpa/reference/jpa/entity-persistence.html

More Spring Boot recipes