Here’s How to Face and Overcome It in Spring Boot
Concurrency issues in applications—especially those backed by relational databases—can cause data inconsistencies, lost updates, or phantom reads that are hard to detect and even harder to debug. Have you ever seen something like:
- A user balance suddenly incorrect?
- A stock quantity going negative even with validation?
- Two administrators editing the same record, with one overwriting the other’s changes?
If yes, you’re not alone. These are classic symptoms of concurrent modification problems. The good news is: JPA gives you tools to overcome them.
In this article, we’ll explore two powerful concurrency control strategies:
- Pessimistic Locking (via
@Lock
) - Optimistic Locking (via
@Version
)
We’ll dive into:
- 🔍 When to use each approach
- 🧪 Practical code examples
- 🧭 How to choose between them
🔒 Pessimistic Locking: The “Reserve Now, Edit Later” Approach
Pessimistic locking assumes conflict is likely. When a transaction reads a record, it immediately locks it, preventing others from making any changes until it’s done.
🧠 When to Use It
- You expect high contention on records (e.g., financial systems).
- Losing an update is unacceptable.
- Transactions involve multiple steps, and integrity must be guaranteed.
🧱 Example: Booking a Seat on a Flight
@Entity
public class Seat {
@Id
private Long id;
private boolean booked;
private String passengerName;
}
Repository with Pessimistic Lock:
public interface SeatRepository extends JpaRepository<Seat, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT s FROM Seat s WHERE s.id = :id")
Optional<Seat> findSeatForUpdate(@Param("id") Long id);
}
Service Example:
@Transactional
public void bookSeat(Long seatId, String passenger) {
Seat seat = seatRepository.findSeatForUpdate(seatId)
.orElseThrow(() -> new RuntimeException("Seat not found"));
if (seat.isBooked()) {
throw new IllegalStateException("Seat already booked");
}
seat.setBooked(true);
seat.setPassengerName(passenger);
seatRepository.save(seat);
}
🧩 How It Works: The database places a row-level lock. Any other transaction trying to update the same row must wait or fail (depending on your DB settings).
✅ Pros:
- Guarantees consistency.
- Easy to understand.
❌ Cons:
- Lower performance under high load.
- Risk of deadlocks if not managed carefully.
🔄 Optimistic Locking: The “Try First, Fail Fast” Approach
Optimistic locking assumes that conflict is rare. Instead of locking the row, it uses a @Version
field to detect changes before committing.
🧠 When to Use It
- Low probability of conflict.
- You want high performance and scalability.
- Conflict resolution is acceptable via retries or user feedback.
🧱 Example: Editing a Product in an Admin Panel
@Entity
public class Product {
@Id
private Long id;
private String name;
private BigDecimal price;
@Version
private Integer version;
}
Service Example:
@Transactional
public void updateProduct(Product updatedProduct) {
Product existing = productRepository.findById(updatedProduct.getId())
.orElseThrow(() -> new RuntimeException("Product not found"));
existing.setName(updatedProduct.getName());
existing.setPrice(updatedProduct.getPrice());
// version is automatically checked when save is called
}
Behavior:
If two users fetch the same product and both try to update it:
- The first one succeeds.
- The second fails with an
OptimisticLockException
, because theversion
no longer matches.
✅ Pros:
- No locks; better throughput.
- Great for read-heavy systems.
❌ Cons:
- You must handle retries.
- Users might lose work unless you give UI feedback.
🧭 Choosing Between Pessimistic vs Optimistic Locking
Feature/Scenario | Pessimistic Locking | Optimistic Locking |
---|---|---|
Conflict likelihood | High | Low |
Critical transactions (e.g., banking) | ✅ Best choice | ❌ Risky |
Performance / scalability | ❌ Blocking, lower throughput | ✅ Non-blocking, faster |
Multi-step operations (e.g., check + update) | ✅ Needed | ❌ Race condition risk |
Distributed or web applications (REST APIs) | ❌ Can lead to timeouts | ✅ More robust |
Need for user feedback on conflict | ❌ Hard to provide | ✅ Easy to detect & inform |
🛠 Final Tip: Always Combine With @Transactional
Both approaches require transactions to be effective. Never use locking logic without @Transactional
, or you’ll break consistency guarantees.
✅ Conclusion
Concurrency problems don’t have to be mysterious or unmanageable. With JPA’s pessimistic and optimistic locking tools, Spring Boot gives you robust options to:
- Prevent data corruption
- Handle conflicts gracefully
- Build resilient systems
🧭 Choose pessimistic locking when data integrity is non-negotiable, and choose optimistic locking when performance and user experience matter most.
📚 Know More: Other Strategies to Handle Concurrency
While optimistic and pessimistic locking are essential tools for managing concurrency in database operations, they’re not the only options. Depending on your system architecture and the type of data you’re protecting, you might benefit from additional or alternative concurrency control techniques.
Here are several other common strategies:
🔄 Software-Level Concurrency Controls
- Synchronized Code Blocks (Java)
- Use
synchronized
orReentrantLock
for in-memory locking. - Best for simple JVM-based state sharing in single-node applications.
- Use
- Application-Level Queuing
- Queue requests (e.g., with a message broker) to serialize access to resources.
- Reduces write contention by delegating processing to a single-threaded consumer.
- Atomic Operations
- Use atomic types like
AtomicInteger
,AtomicReference
, orConcurrentHashMap
for low-level concurrent data manipulation.
- Use atomic types like
- Java Concurrency Utilities
- Use
Semaphore
,CountDownLatch
,ExecutorService
, etc., for precise thread-level control.
- Use
🌐 Distributed Concurrency Controls
- Distributed Locks
- Leverage Redis (e.g., Redisson), Zookeeper, or a database table to coordinate access across application nodes.
- Suitable for microservices or clustered environments.
- ShedLock for Scheduled Tasks
- Prevents multiple instances of the same Spring Boot app from executing a scheduled task concurrently.
- Commonly used in distributed cron jobs.
📊 Comparison Table: Concurrency Strategies at a Glance
Strategy | Scope | Blocking? | Distributed? | Use Case Example |
---|---|---|---|---|
Optimistic Locking | Database row | No | Yes | Editing product details in a CMS |
Pessimistic Locking | Database row | Yes | Yes | Booking a flight seat |
Synchronized / ReentrantLock | JVM memory | Yes | No | Accessing shared cache in a single-node app |
Atomic Variables | JVM memory | No | No | Updating counters, flags |
Distributed Lock (Redis/ZK) | Global cluster | Yes | Yes | Updating shared resources across nodes |
Application Queue (RabbitMQ, Kafka) | Service-level | N/A | Yes | Processing bank transactions sequentially |
ShedLock | Scheduled tasks | Yes | Yes | Cron jobs in a multi-node Spring Boot app |
🧭 Final Thoughts
Understanding when and how to use each strategy is key to building reliable systems:
- For data consistency, start with JPA locking mechanisms.
- For application-level control, use Java concurrency tools or queues.
- For clustered environments, distributed locking or queuing is essential.
Combine these techniques thoughtfully based on your system’s architecture, performance goals, and failure tolerance.