Resolving Deadlocks in Transactions in Spring Boot
In transactional systems, deadlocks are a common concurrency issue, particularly when several threads or processes seek for the same resources. Deadlocks can cause major problems with application speed and consistency in Spring Boot applications that use relational databases and @Transactional annotations. In this blog post, we will cover: What a deadlock is How it occurs in Spring Boot transactional systems Real code examples How to detect and handle deadlocks Best practices to prevent them What is a Deadlock? A deadlock occurs when two or more transactions block each other by holding a lock and waiting for the other’s lock to be released. Example: Transaction A locks Row 1 and needs Row 2 Transaction B locks Row 2 and needs Row 1 Neither can proceed, resulting in a deadlock Relational databases (like MySQL, PostgreSQL) will detect the deadlock and typically abort one of the transactions, throwing an exception. Deadlock in Spring Boot Example Let’s consider a simple scenario using Spring Data JPA and @Transactional. Model : @Entity public class Account { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; private BigDecimal balance; // getters and setters } Repository : public interface AccountRepository extends JpaRepository {} Service with Transaction : @Service public class AccountService { @Autowired private AccountRepository accountRepository; //this method will cause deadlocks if two threads access the transfer method at the same time. //If both threads fetch and lock their first account row and then, //attempt to access the other, a deadlock is likely. @Transactional public void transfer(Long fromId, Long toId, BigDecimal amount) { Account from = accountRepository.findById(fromId).orElseThrow(); Account to = accountRepository.findById(toId).orElseThrow(); from.setBalance(from.getBalance().subtract(amount)); to.setBalance(to.getBalance().add(amount)); accountRepository.save(from); accountRepository.save(to); } Thread A: transfers from Account 1 to 2 Thread B: transfers from Account 2 to 1 How to Detect Deadlocks 1. Database Logs Databases like MySQL and PostgreSQL will log deadlocks. For example, in MySQL, you can run: SHOW ENGINE INNODB STATUS; 2. Application Logs Spring will typically throw a DeadlockLoserDataAccessException or a database-specific exception. Make sure your logs include stack traces. How to Handle and Prevent Deadlocks 1. Consistent Lock Ordering Always access resources in the same order. In the above example, always load accounts in order of ID: @Transactional public void transfer(Long fromId, Long toId, BigDecimal amount) { List accounts = accountRepository.findAllById(List.of(fromId, toId)); Account from = fromId < toId ? accounts.get(0) : accounts.get(1); Account to = fromId < toId ? accounts.get(1) : accounts.get(0); accountRepository.save(from); accountRepository.save(to); } 2. Retry Logic on Deadlocks Use Spring’s @Transactional with retry logic using @Retryable from Spring Retry: @Retryable( value = {TransientDataAccessException.class}, maxAttempts = 3, backoff = @Backoff(delay = 200)) @Transactional public void transfer2(Long fromId, Long toId, BigDecimal amount) { Account from = accountRepository.findById(fromId).orElseThrow(); Account to = accountRepository.findById(toId).orElseThrow(); from.setBalance(from.getBalance().subtract(amount)); to.setBalance(to.getBalance().add(amount)); accountRepository.save(from); accountRepository.save(to); } Don’t forget to enable Spring Retry: @EnableRetry @SpringBootApplication public class DeadlockexampleApplication { public static void main(String[] args) { SpringApplication.run(DeadlockexampleApplication.class, args); } } value and include are deprecated in latest versions so you can Define retry behavior via a RetryPolicy bean. @Configuration @EnableRetry public class RetryConfig { @Bean public RetryPolicy retryPolicy() { SimpleRetryPolicy policy = new SimpleRetryPolicy(); policy.setMaxAttempts(3); policy.setRetryableExceptions(Map.of( TransientDataAccessException.class, true )); return policy; } @Bean public RetryTemplate retryTemplate(RetryPolicy retryPolicy) { RetryTemplate template = new RetryTemplate(); template.setRetryPolicy(retryPolicy); return template; } } 3. Locking Strategies Repository: @Repository public interface AccountRepository extends JpaRepository { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT a FROM Account a WHERE a.id

In transactional systems, deadlocks are a common concurrency issue, particularly when several threads or processes seek for the same resources. Deadlocks can cause major problems with application speed and consistency in Spring Boot applications that use relational databases and @Transactional annotations.
In this blog post, we will cover:
- What a deadlock is
- How it occurs in Spring Boot transactional systems
- Real code examples
- How to detect and handle deadlocks
- Best practices to prevent them
What is a Deadlock?
A deadlock occurs when two or more transactions block each other by holding a lock and waiting for the other’s lock to be released.
Example:
- Transaction A locks Row 1 and needs Row 2
- Transaction B locks Row 2 and needs Row 1
- Neither can proceed, resulting in a deadlock
Relational databases (like MySQL, PostgreSQL) will detect the deadlock and typically abort one of the transactions, throwing an exception.
Deadlock in Spring Boot Example
Let’s consider a simple scenario using Spring Data JPA and @Transactional.
Model :
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private BigDecimal balance;
// getters and setters
}
Repository :
public interface AccountRepository extends JpaRepository {}
Service with Transaction :
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
//this method will cause deadlocks if two threads access the transfer method at the same time.
//If both threads fetch and lock their first account row and then,
//attempt to access the other, a deadlock is likely.
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
}
Thread A: transfers from Account 1 to 2
Thread B: transfers from Account 2 to 1
How to Detect Deadlocks
1. Database Logs
Databases like MySQL and PostgreSQL will log deadlocks. For example, in MySQL, you can run:
SHOW ENGINE INNODB STATUS;
2. Application Logs
Spring will typically throw a DeadlockLoserDataAccessException or a database-specific exception. Make sure your logs include stack traces.
How to Handle and Prevent Deadlocks
1. Consistent Lock Ordering
Always access resources in the same order. In the above example, always load accounts in order of ID:
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
List accounts = accountRepository.findAllById(List.of(fromId, toId));
Account from = fromId < toId ? accounts.get(0) : accounts.get(1);
Account to = fromId < toId ? accounts.get(1) : accounts.get(0);
accountRepository.save(from);
accountRepository.save(to);
}
2. Retry Logic on Deadlocks
Use Spring’s @Transactional with retry logic using @Retryable from Spring Retry:
@Retryable(
value = {TransientDataAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 200))
@Transactional
public void transfer2(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
}
Don’t forget to enable Spring Retry:
@EnableRetry
@SpringBootApplication
public class DeadlockexampleApplication {
public static void main(String[] args) {
SpringApplication.run(DeadlockexampleApplication.class, args);
}
}
- value and include are deprecated in latest versions so you can Define retry behavior via a RetryPolicy bean.
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryPolicy retryPolicy() {
SimpleRetryPolicy policy = new SimpleRetryPolicy();
policy.setMaxAttempts(3);
policy.setRetryableExceptions(Map.of(
TransientDataAccessException.class, true
));
return policy;
}
@Bean
public RetryTemplate retryTemplate(RetryPolicy retryPolicy) {
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(retryPolicy);
return template;
}
}
3. Locking Strategies
Repository:
@Repository
public interface AccountRepository extends JpaRepository {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdForUpdate(@Param("id") Long id);
}
Service:
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findByIdForUpdate(fromId);
Account to = accountRepository.findByIdForUpdate(toId);
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
}
Important Notes:
Pessimistic locking tells the database to acquire a row-level write lock as soon as the row is read — no other transaction can read/write it until the current one commits or rolls back.
This works well to prevent deadlocks, but can cause blocking and contention under high load. So use it carefully.
Always make sure you’re using it inside a @Transactional method — otherwise, the lock will be released immediately after the method exits.
4. Keep Transactions Short
Avoid long-running transactions. Don’t perform unnecessary logic inside a transaction block.
Conclusion
Deadlocks are inevitable in highly concurrent systems, but with careful design, detection, and retries, you can manage them effectively in Spring Boot. Stick to consistent access patterns, use proper locking, and ensure your application is resilient to deadlocks through retries and monitoring.
References
https://www.baeldung.com/spring-retry
https://www.baeldung.com/java-deadlock-livelock
https://www.baeldung.com/transaction-configuration-with-jpa-and-spring
GitHub : https://github.com/tharindu1998/resolve-deadlock-example