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

May 5, 2025 - 06:36
 0
Resolving Deadlocks in Transactions in Spring Boot

Image description

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