Deadlocks are among the most frustrating issues in database-driven applications. They occur when two or more transactions permanently block each other, each waiting for a resource that the other transaction holds. In Spring-based systems — especially those using JPA, Hibernate, or direct JDBC — a deadlock can cause unexpected timeouts, slowdowns, and even application crashes.
While modern relational databases like PostgreSQL, MySQL, and Oracle are capable of detecting and resolving deadlocks automatically by rolling back one transaction, the real challenge for developers lies in identifying the root cause and preventing recurrence.
This guide explores practical methods to find, detect, and prevent potential database deadlocks in Spring applications, along with examples, tools, and best practices.
1. What Causes a Deadlock?
A database deadlock typically arises when transactions acquire locks on multiple resources in an inconsistent order.
For example:
| Transaction 1 | Transaction 2 |
|---|---|
Locks table A | Locks table B |
Waits for table B | Waits for table A |
Neither transaction can proceed because each one holds a lock that the other needs.
In Spring applications, this can happen due to:
- Concurrent updates on the same database row.
- Poor transaction management (e.g., long-running transactions).
- Inconsistent ordering of update operations.
- Incorrect isolation levels or propagation settings.
2. How to Detect Deadlocks in Spring Applications
a) Enable SQL and Transaction Logging
Spring and Hibernate provide configuration options to log executed SQL statements. Enabling these logs helps identify overlapping operations that could cause locking conflicts.
# application.properties
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.transaction=TRACE
You can also trace the transaction boundaries:
@Transactional
public void transferFunds(Long fromAccount, Long toAccount, double amount) {
log.info("Starting transaction...");
Account source = accountRepository.findById(fromAccount).orElseThrow();
Account destination = accountRepository.findById(toAccount).orElseThrow();
source.debit(amount);
destination.credit(amount);
accountRepository.save(source);
accountRepository.save(destination);
log.info("Transaction committed successfully.");
}
If two concurrent transactions invoke this method with overlapping accounts, you might see a lock contention or deadlock error in the logs.
b) Use Database Diagnostic Tools
Each database engine offers built-in tools to detect recent deadlocks:
MySQL
SHOW ENGINE INNODB STATUS;
PostgreSQL
Check the PostgreSQL log for entries like:
ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 987; blocked by process 54321.
Oracle
Query the DBA_BLOCKERS and DBA_WAITERS views:
SELECT * FROM dba_blockers;
SELECT * FROM dba_waiters;
These views reveal which sessions are blocking each other.
c) Use a Java Profiler or Thread Dump
Java profilers such as JProfiler, YourKit, or VisualVM can detect thread-level deadlocks, showing which threads are waiting on monitors or synchronized blocks.
Example:
Found one Java-level deadlock:
"Thread-1":
waiting to lock monitor 0x000000001c8b7cd8 (object com.example.Account@75f9f7ce)
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x000000001c8b7d20 (object com.example.Account@58a9760)
which is held by "Thread-1"
If you see such a trace, review your synchronized code blocks and transactional logic.
d) Spring Boot Actuator and Health Endpoints
Spring Boot Actuator can provide insights into your application’s runtime health, including database performance metrics. Although it won’t directly show deadlocks, it helps monitor symptoms like transaction timeouts and thread pool exhaustion.
Enable it in your project:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
Then access:
/actuator/health
/actuator/metrics
3. How to Prevent Database Deadlocks
a) Keep Transactions Short
The longer a transaction holds a lock, the greater the chance of collision. Ensure that database operations are concise and avoid unnecessary computations inside transactional blocks.
Bad:
@Transactional
public void updateInventorySlow() {
Thread.sleep(10000); // expensive operation inside transaction
productRepository.updateStock(123, 10);
}
Good:
public void updateInventory() {
performExpensiveCalculation();
updateStockTransactional();
}
@Transactional
protected void updateStockTransactional() {
productRepository.updateStock(123, 10);
}
b) Maintain Consistent Lock Ordering
Always acquire resources in the same order across transactions.
For example, if one part of the code updates Account A before Account B, ensure no other transaction updates B before A.
c) Choose the Right Isolation Level
Spring allows you to set transaction isolation levels that determine how concurrent access is managed.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processOrder(Long orderId) {
// business logic here
}
Avoid using overly strict levels like SERIALIZABLE unless absolutely necessary, as they can increase lock contention.
d) Implement Retry Logic
When a deadlock occurs, databases typically roll back one of the transactions. You can catch the exception and retry the operation safely.
Example:
public void safeTransfer(Long from, Long to, double amount) {
for (int i = 0; i < 3; i++) {
try {
transferFunds(from, to, amount);
return;
} catch (CannotAcquireLockException | DeadlockLoserDataAccessException e) {
log.warn("Deadlock detected, retrying... attempt {}", i + 1);
}
}
throw new RuntimeException("Failed to complete transfer after 3 attempts");
}
e) Use Database Indexes and Optimistic Locking
Add proper indexes to speed up queries and reduce lock duration.
With JPA, you can also use optimistic locking by adding a @Version column:
@Entity
public class Product {
@Id
private Long id;
private int stock;
@Version
private Long version;
}
Optimistic locking avoids database-level locks and uses version checks instead.
4. Tools for Continuous Monitoring
| Tool | Description |
|---|---|
| SonarQube | Detects potential concurrency issues in your code via static analysis. |
| Prometheus & Grafana | Collect and visualize database metrics to identify patterns leading to deadlocks. |
| New Relic / AppDynamics / Dynatrace | Application Performance Monitoring (APM) tools that automatically detect deadlocks and slow queries. |
| VisualVM / JProfiler / YourKit | Java profilers that can visualize thread contention and synchronization issues. |
Conclusion
Database deadlocks are inevitable in complex systems where multiple transactions compete for the same resources. The key to managing them lies in detection, prevention, and resilience.
By combining proper transaction management, consistent lock ordering, logging, and retry mechanisms, you can build robust Spring applications that gracefully handle concurrency under load.
If you’re building a high-performance enterprise application, make deadlock analysis a part of your continuous integration process — not just your post-failure debugging.


